Merge "Introduce a new flag to enable crashing the app on FGS timeout." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 0ccdf37..ab5d503 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1043,20 +1043,12 @@
     name: "device_policy_aconfig_flags",
     package: "android.app.admin.flags",
     container: "system",
-    exportable: true,
     srcs: [
         "core/java/android/app/admin/flags/flags.aconfig",
     ],
 }
 
 java_aconfig_library {
-    name: "device_policy_exported_aconfig_flags_lib",
-    aconfig_declarations: "device_policy_aconfig_flags",
-    defaults: ["framework-minus-apex-aconfig-java-defaults"],
-    mode: "exported",
-}
-
-java_aconfig_library {
     name: "device_policy_aconfig_flags_lib",
     aconfig_declarations: "device_policy_aconfig_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/CleanSpec.mk b/CleanSpec.mk
index 02e8eec..e680103 100644
--- a/CleanSpec.mk
+++ b/CleanSpec.mk
@@ -262,6 +262,7 @@
 $(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/priv-app/InProcessTethering)
 $(call add-clean-step, rm -rf $(PRODUCT_OUT)/system/app/OsuLogin)
 $(call add-clean-step, rm -rf $(PRODUCT_OUT)/system_other/system/app/OsuLogin)
+$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime)
 # ******************************************************************
 # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER
 # ******************************************************************
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 255ec92..74b34fb 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -246,12 +246,20 @@
     visibility: ["//visibility:private"],
 }
 
+java_genrule {
+    name: "z00-all-updatable-modules-system-stubs",
+    cmd: "cp $(in) $(out)",
+    srcs: [":all-updatable-modules-system-stubs"],
+    out: ["z00-all-updatable-modules-system-stubs.jar"],
+    visibility: ["//visibility:private"],
+}
+
 android_ravenwood_libgroup {
     name: "ravenwood-runtime",
     libs: [
         "100-framework-minus-apex.ravenwood",
         "200-kxml2-android",
-        "all-updatable-modules-system-stubs",
+
         "android.test.mock.ravenwood",
         "ravenwood-helper-runtime",
         "hoststubgen-helper-runtime.ravenwood",
@@ -267,6 +275,9 @@
         "ravenwood-junit-impl-flag",
         "mockito-ravenwood-prebuilt",
         "inline-mockito-ravenwood-prebuilt",
+
+        // It's a stub, so it should be towards the end.
+        "z00-all-updatable-modules-system-stubs",
     ],
     jni_libs: [
         "libandroid_runtime",
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
index a4a2e80..9b0f5c9 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
@@ -133,7 +133,7 @@
         pw.println("      --tag: Tag of the blob to delete.");
         pw.println("idle-maintenance");
         pw.println("    Run idle maintenance which takes care of removing stale data.");
-        pw.println("query-blob-existence [-b BLOB_ID]");
+        pw.println("query-blob-existence [-b BLOB_ID] [-u | --user USER_ID]");
         pw.println("    Prints 1 if blob exists, otherwise 0.");
         pw.println();
     }
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index d59d430..ad54cd39 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -491,8 +491,10 @@
      * Returns a list of all currently-executing jobs.
      * @hide
      */
-    @SuppressWarnings("HiddenAbstractMethod")
-    public abstract List<JobInfo> getStartedJobs();
+    @Nullable
+    public List<JobInfo> getStartedJobs() {
+        return null;
+    }
 
     /**
      * <b>For internal system callers only!</b>
@@ -501,8 +503,10 @@
      * <p class="note">This is a slow operation, so it should be called sparingly.
      * @hide
      */
-    @SuppressWarnings("HiddenAbstractMethod")
-    public abstract List<JobSnapshot> getAllJobSnapshots();
+    @Nullable
+    public List<JobSnapshot> getAllJobSnapshots() {
+        return null;
+    }
 
     /**
      * @hide
@@ -510,8 +514,8 @@
     @RequiresPermission(allOf = {
             android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
             android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
-    @SuppressWarnings("HiddenAbstractMethod")
-    public abstract void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer);
+    public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+    }
 
     /**
      * @hide
@@ -519,9 +523,10 @@
     @RequiresPermission(allOf = {
             android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
             android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
-    @SuppressWarnings("HiddenAbstractMethod")
-    public abstract void unregisterUserVisibleJobObserver(
-            @NonNull IUserVisibleJobObserver observer);
+    public void unregisterUserVisibleJobObserver(
+            @NonNull IUserVisibleJobObserver observer) {
+
+    }
 
     /**
      * @hide
@@ -529,7 +534,7 @@
     @RequiresPermission(allOf = {
             android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
             android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
-    @SuppressWarnings("HiddenAbstractMethod")
-    public abstract void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
-            @Nullable String debugReason);
+    public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+            @Nullable String debugReason) {
+    }
 }
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index e20f525..e489c1a 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -38,3 +38,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+   name: "thermal_restrictions_to_fgs_jobs"
+   namespace: "backstage_power"
+   description: "Apply thermal restrictions to FGS jobs."
+   bug: "315157163"
+   metadata {
+       purpose: PURPOSE_BUGFIX
+   }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 3bb395f..ba8e3e8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1375,8 +1375,10 @@
             final JobServiceContext jsc = mActiveServices.get(i);
             final JobStatus jobStatus = jsc.getRunningJobLocked();
 
-            if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
-                    && restriction.isJobRestricted(jobStatus)) {
+            if (jobStatus != null
+                    && !jsc.isWithinExecutionGuaranteeTime()
+                    && restriction.isJobRestricted(
+                            jobStatus, mService.evaluateJobBiasLocked(jobStatus))) {
                 jsc.cancelExecutingJobLocked(restriction.getStopReason(),
                         restriction.getInternalReason(),
                         JobParameters.getInternalReasonCodeDescription(
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 5d1433c..384d786 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -310,7 +310,8 @@
      * Note: do not add to or remove from this list at runtime except in the constructor, because we
      * do not synchronize access to this list.
      */
-    private final List<JobRestriction> mJobRestrictions;
+    @VisibleForTesting
+    final List<JobRestriction> mJobRestrictions;
 
     @GuardedBy("mLock")
     @VisibleForTesting
@@ -3498,8 +3499,6 @@
 
     /**
      * Check if a job is restricted by any of the declared {@link JobRestriction JobRestrictions}.
-     * Note, that the jobs with {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher may not
-     * be restricted, thus we won't even perform the check, but simply return null early.
      *
      * @param job to be checked
      * @return the first {@link JobRestriction} restricting the given job that has been found; null
@@ -3508,13 +3507,9 @@
      */
     @GuardedBy("mLock")
     JobRestriction checkIfRestricted(JobStatus job) {
-        if (evaluateJobBiasLocked(job) >= JobInfo.BIAS_FOREGROUND_SERVICE) {
-            // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted
-            return null;
-        }
         for (int i = mJobRestrictions.size() - 1; i >= 0; i--) {
             final JobRestriction restriction = mJobRestrictions.get(i);
-            if (restriction.isJobRestricted(job)) {
+            if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) {
                 return restriction;
             }
         }
@@ -4221,6 +4216,7 @@
         return curBias;
     }
 
+    /** Gets and returns the adjusted Job Bias **/
     int evaluateJobBiasLocked(JobStatus job) {
         int bias = job.getBias();
         if (bias >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) {
@@ -5907,7 +5903,7 @@
                     if (isRestricted) {
                         for (int i = mJobRestrictions.size() - 1; i >= 0; i--) {
                             final JobRestriction restriction = mJobRestrictions.get(i);
-                            if (restriction.isJobRestricted(job)) {
+                            if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) {
                                 final int reason = restriction.getInternalReason();
                                 pw.print(" ");
                                 pw.print(JobParameters.getInternalReasonCodeDescription(reason));
@@ -6240,7 +6236,7 @@
                         proto.write(JobSchedulerServiceDumpProto.JobRestriction.REASON,
                                 restriction.getInternalReason());
                         proto.write(JobSchedulerServiceDumpProto.JobRestriction.IS_RESTRICTING,
-                                restriction.isJobRestricted(job));
+                                restriction.isJobRestricted(job, evaluateJobBiasLocked(job)));
                         proto.end(restrictionsToken);
                     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 852b00b..d5a58d1 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -1771,7 +1771,13 @@
                     final int logicalIndex = mapping.getLogicalSlotIndex();
                     if (mCarrierPrivilegedCallbacks.contains(logicalIndex)) {
                         // Callback already exists. No need to create a new one or remove it.
-                        callbacksToRemove.remove(logicalIndex);
+                        for (int i = callbacksToRemove.size() - 1; i >= 0; i--) {
+                            if (callbacksToRemove.get(i) == logicalIndex) {
+                                callbacksToRemove.remove(i);
+                                break;
+                            }
+                        }
+
                         continue;
                     }
                     final LogicalIndexCarrierPrivilegesCallback callback =
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
index 7aab67a..555a118 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -62,10 +62,11 @@
      * fine with it).
      *
      * @param job to be checked
+     * @param bias job bias to be checked
      * @return false if the {@link JobSchedulerService} should not schedule this job at the moment,
      * true - otherwise
      */
-    public abstract boolean isJobRestricted(JobStatus job);
+    public abstract boolean isJobRestricted(JobStatus job, int bias);
 
     /** Dump any internal constants the Restriction may have. */
     public abstract void dumpConstants(IndentingPrintWriter pw);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index ef634b5..ba01113 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -24,6 +24,7 @@
 import android.util.IndentingPrintWriter;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.job.Flags;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.controllers.JobStatus;
 
@@ -85,7 +86,18 @@
     }
 
     @Override
-    public boolean isJobRestricted(JobStatus job) {
+    public boolean isJobRestricted(JobStatus job, int bias) {
+        if (Flags.thermalRestrictionsToFgsJobs()) {
+            if (bias >= JobInfo.BIAS_TOP_APP) {
+                // Jobs with BIAS_TOP_APP should not be restricted
+                return false;
+            }
+        } else {
+            if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) {
+                // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted
+                return false;
+            }
+        }
         if (mThermalStatus >= UPPER_THRESHOLD) {
             return true;
         }
@@ -107,6 +119,17 @@
                         || (mService.isCurrentlyRunningLocked(job)
                                 && mService.isJobInOvertimeLocked(job));
             }
+            if (Flags.thermalRestrictionsToFgsJobs()) {
+                // Only let foreground jobs run if:
+                // 1. They haven't previously run
+                // 2. They're already running and aren't yet in overtime
+                if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE
+                        && job.getJob().isImportantWhileForeground()) {
+                    return job.getNumPreviousAttempts() > 0
+                            || (mService.isCurrentlyRunningLocked(job)
+                                    && mService.isJobInOvertimeLocked(job));
+                }
+            }
             if (priority == JobInfo.PRIORITY_HIGH) {
                 return !mService.isCurrentlyRunningLocked(job)
                         || mService.isJobInOvertimeLocked(job);
@@ -114,6 +137,13 @@
             return true;
         }
         if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) {
+            if (Flags.thermalRestrictionsToFgsJobs()) {
+                if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) {
+                    // No restrictions on foreground jobs
+                    // on LOW_PRIORITY_THRESHOLD and below
+                    return false;
+                }
+            }
             // For light throttling, throttle all min priority jobs and all low priority jobs that
             // aren't already running or have been running for long enough.
             return priority == JobInfo.PRIORITY_MIN
diff --git a/cmds/bootanimation/FORMAT.md b/cmds/bootanimation/FORMAT.md
index 01e8fe1..da8331a 100644
--- a/cmds/bootanimation/FORMAT.md
+++ b/cmds/bootanimation/FORMAT.md
@@ -126,7 +126,7 @@
 Use `zopflipng` if you have it, otherwise `pngcrush` will do. e.g.:
 
     for fn in *.png ; do
-        zopflipng -m ${fn}s ${fn}s.new && mv -f ${fn}s.new ${fn}
+        zopflipng -m ${fn} ${fn}.new && mv -f ${fn}.new ${fn}
         # or: pngcrush -q ....
     done
 
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 2437be8..211fc96 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -966,7 +966,6 @@
     ctor public AttributionSource(int, @Nullable String, @Nullable String);
     ctor public AttributionSource(int, @Nullable String, @Nullable String, @NonNull android.os.IBinder);
     ctor public AttributionSource(int, @Nullable String, @Nullable String, @Nullable java.util.Set<java.lang.String>, @Nullable android.content.AttributionSource);
-    ctor @FlaggedApi("android.permission.flags.attribution_source_constructor") public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], @Nullable android.content.AttributionSource);
     ctor @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], int, @Nullable android.content.AttributionSource);
     method public void enforceCallingPid();
   }
@@ -1775,16 +1774,16 @@
   }
 
   public final class InputManager {
-    method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociation(@NonNull String, @NonNull String);
     method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociationByDescriptor(@NonNull String, @NonNull String);
+    method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void addUniqueIdAssociationByPort(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings();
     method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptors();
     method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String);
     method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping();
     method public int getMousePointerSpeed();
     method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int);
-    method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociation(@NonNull String);
     method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByDescriptor(@NonNull String);
+    method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByPort(@NonNull String);
     field public static final long BLOCK_UNTRUSTED_TOUCHES = 158002302L; // 0x96aec7eL
   }
 
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index c4fe061..6cc71e5 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -975,14 +975,14 @@
     Method 'getHdmiCecVersion' documentation mentions permissions already declared by @RequiresPermission
 RequiresPermission: android.hardware.hdmi.HdmiControlManager#setHdmiCecVersion(int):
     Method 'setHdmiCecVersion' documentation mentions permissions already declared by @RequiresPermission
-RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociation(String, String):
-    Method 'addUniqueIdAssociation' documentation mentions permissions without declaring @RequiresPermission
 RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociationByDescriptor(String, String):
     Method 'addUniqueIdAssociationByDescriptor' documentation mentions permissions already declared by @RequiresPermission
-RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociation(String):
-    Method 'removeUniqueIdAssociation' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.hardware.input.InputManager#addUniqueIdAssociationByPort(String, String):
+    Method 'addUniqueIdAssociationByPort' documentation mentions permissions already declared by @RequiresPermission
 RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociationByDescriptor(String):
     Method 'removeUniqueIdAssociationByDescriptor' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.hardware.input.InputManager#removeUniqueIdAssociationByPort(String):
+    Method 'removeUniqueIdAssociationByPort' documentation mentions permissions already declared by @RequiresPermission
 RequiresPermission: android.hardware.location.GeofenceHardware#addGeofence(int, int, android.hardware.location.GeofenceHardwareRequest, android.hardware.location.GeofenceHardwareCallback):
     Method 'addGeofence' documentation mentions permissions without declaring @RequiresPermission
 RequiresPermission: android.hardware.location.GeofenceHardware#getMonitoringTypes():
@@ -2035,6 +2035,10 @@
     New API must be flagged with @FlaggedApi: method android.content.pm.UserInfo.isPrivateProfile()
 UnflaggedApi: android.credentials.CredentialProviderInfo#isPrimary():
     New API must be flagged with @FlaggedApi: method android.credentials.CredentialProviderInfo.isPrimary()
+UnflaggedApi: android.hardware.input.InputManager#addUniqueIdAssociationByPort(String, String):
+    New API must be flagged with @FlaggedApi: method android.hardware.input.InputManager.addUniqueIdAssociationByPort(String,String)
+UnflaggedApi: android.hardware.input.InputManager#removeUniqueIdAssociationByPort(String):
+    New API must be flagged with @FlaggedApi: method android.hardware.input.InputManager.removeUniqueIdAssociationByPort(String)
 UnflaggedApi: android.media.AudioManager#enterAudioFocusFreezeForTest(java.util.List<java.lang.Integer>):
     New API must be flagged with @FlaggedApi: method android.media.AudioManager.enterAudioFocusFreezeForTest(java.util.List<java.lang.Integer>)
 UnflaggedApi: android.media.AudioManager#exitAudioFocusFreezeForTest():
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index be8f48d..c8ab260 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -90,13 +90,6 @@
     public static final int RESIZE_MODE_USER = RESIZE_MODE_PRESERVE_WINDOW;
 
     /**
-     * Input parameter to {@link IActivityTaskManager#resizeTask} used by window
-     * manager during a screen rotation.
-     * @hide
-     */
-    public static final int RESIZE_MODE_SYSTEM_SCREEN_ROTATION = RESIZE_MODE_PRESERVE_WINDOW;
-
-    /**
      * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
      * that the resize should be performed even if the bounds appears unchanged.
      * @hide
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c3bac71..0caea7f 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -114,6 +114,7 @@
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ContrastColorUtil;
+import com.android.internal.util.NewlineNormalizer;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -3190,7 +3191,7 @@
             return charSequence;
         }
 
-        return charSequence.toString().replaceAll("[\r\n]+", "\n");
+        return NewlineNormalizer.normalizeNewlines(charSequence.toString());
     }
 
     private static CharSequence removeTextSizeSpans(CharSequence charSequence) {
@@ -6490,7 +6491,8 @@
         // visual regressions.
         @SuppressWarnings("AndroidFrameworkCompatChange")
         private boolean bigContentViewRequired() {
-            if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
+            if (!Flags.notificationExpansionOptional()
+                    && mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
                 return true;
             }
             // Notifications with contentView and without a bigContentView, style, or actions would
@@ -6593,12 +6595,12 @@
          * @hide
          */
         public RemoteViews createCompactHeadsUpContentView() {
-            // TODO(b/336225281): re-evaluate custom view usage.
-            if (useExistingRemoteView(mN.headsUpContentView)) {
-                return fullyCustomViewRequiresDecoration(false /* fromStyle */)
-                        ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView)
-                        : mN.headsUpContentView;
-            } else if (mStyle != null) {
+            // Don't show compact heads up for FSI notifications.
+            if (mN.fullScreenIntent != null) {
+                return createHeadsUpContentView(/* increasedHeight= */ false);
+            }
+
+            if (mStyle != null) {
                 final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView();
                 if (styleView != null) {
                     return styleView;
@@ -6611,7 +6613,7 @@
             // Notification text is shown as secondary header text
             // for the minimal hun when it is provided.
             // Time(when and chronometer) is not shown for the minimal hun.
-            p.headerTextSecondary(p.mText).text(null).hideTime(true);
+            p.headerTextSecondary(p.mText).text(null).hideTime(true).summaryText("");
 
             return applyStandardTemplate(
                     getCompactHeadsUpBaseLayoutResource(), p,
@@ -10355,7 +10357,7 @@
         @Nullable
         @Override
         public RemoteViews makeCompactHeadsUpContentView() {
-            // TODO(b/336228700): Apply minimal HUN treatment for Call Style.
+            // Use existing heads up for call style.
             return makeHeadsUpContentView(false);
         }
 
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index efd5a45..ef8501f 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -351,6 +351,12 @@
     }
 
     /** @hide */
+    public boolean isFreeform() {
+        return configuration.windowConfiguration.getWindowingMode()
+                == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+    }
+
+    /** @hide */
     @WindowConfiguration.ActivityType
     public int getActivityType() {
         return configuration.windowConfiguration.getActivityType();
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 46c9e78..9ef8b38 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -21,7 +21,6 @@
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.app.admin.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -177,10 +176,6 @@
      * provisioned into "affiliated" mode when on a Headless System User Mode device.
      *
      * <p>This mode adds a Profile Owner to all users other than the user the Device Owner is on.
-     *
-     * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
-     * DPCs should set the value of attribute "headless-device-owner-mode" inside the
-     * "headless-system-user" tag as "affiliated".
      */
     public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;
 
@@ -190,10 +185,6 @@
      *
      * <p>This mode only allows a single secondary user on the device blocking the creation of
      * additional secondary users.
-     *
-     * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
-     * DPCs should set the value of attribute "headless-device-owner-mode" inside the
-     * "headless-system-user" tag as "single_user".
      */
     @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
     public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
@@ -392,30 +383,17 @@
                     }
                     mSupportsTransferOwnership = true;
                 } else if (tagName.equals("headless-system-user")) {
-                    String deviceOwnerModeStringValue = null;
-                    if (Flags.headlessSingleUserCompatibilityFix()) {
-                        deviceOwnerModeStringValue = parser.getAttributeValue(
-                                 null, "headless-device-owner-mode");
-                    }
-                    if (deviceOwnerModeStringValue == null) {
-                        deviceOwnerModeStringValue =
-                                parser.getAttributeValue(null, "device-owner-mode");
-                    }
+                    String deviceOwnerModeStringValue =
+                            parser.getAttributeValue(null, "device-owner-mode");
 
-                    if ("unsupported".equalsIgnoreCase(deviceOwnerModeStringValue)) {
+                    if (deviceOwnerModeStringValue.equalsIgnoreCase("unsupported")) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
-                    } else if ("affiliated".equalsIgnoreCase(deviceOwnerModeStringValue)) {
+                    } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
-                    } else if ("single_user".equalsIgnoreCase(deviceOwnerModeStringValue)) {
+                    } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) {
                         mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
                     } else {
-                        if (Flags.headlessSingleUserCompatibilityFix()) {
-                            Log.e(TAG, "Unknown headless-system-user mode: "
-                                    + deviceOwnerModeStringValue);
-                        } else {
-                            throw new XmlPullParserException(
-                                    "headless-system-user mode must be valid");
-                        }
+                        throw new XmlPullParserException("headless-system-user mode must be valid");
                     }
                 }
             }
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 83daa45..6da96c1 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -217,6 +217,16 @@
 }
 
 flag {
+  name: "disallow_user_control_stopped_state_fix"
+  namespace: "enterprise"
+  description: "Ensure DPM.setUserControlDisabledPackages() clears FLAG_STOPPED for the app"
+  bug: "330688482"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "esim_management_ux_enabled"
   namespace: "enterprise"
   description: "Enable UX changes for esim management"
@@ -303,24 +313,3 @@
       purpose: PURPOSE_BUGFIX
     }
 }
-
-flag {
-    name: "headless_single_user_compatibility_fix"
-    namespace: "enterprise"
-    description: "Fix for compatibility issue introduced from using single_user mode on pre-Android V builds"
-    bug: "338050276"
-    is_exported: true
-    metadata {
-      purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    name: "headless_single_min_target_sdk"
-    namespace: "enterprise"
-    description: "Only allow DPCs targeting Android V to provision into single user mode"
-    bug: "338588825"
-    metadata {
-      purpose: PURPOSE_BUGFIX
-    }
-}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 63ffaa0..50c7b7f 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -60,6 +60,13 @@
 }
 
 flag {
+  name: "notification_expansion_optional"
+  namespace: "systemui"
+  description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions."
+  bug: "339523906"
+}
+
+flag {
   name: "keyguard_private_notifications"
   namespace: "systemui"
   description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()"
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index c7b168a..04c3686 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -47,3 +47,14 @@
     description: "Feature flag for collecting app data size by file type API"
     bug: "294088945"
 }
+
+flag {
+    name: "disable_idle_check"
+    namespace: "backstage_power"
+    description: "disable idle check for USER_SYSTEM during boot up"
+    is_fixed_read_only: true
+    bug: "337864590"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index b070742..37f419d 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -162,17 +162,6 @@
 
     /** @hide */
     @TestApi
-    @FlaggedApi(Flags.FLAG_ATTRIBUTION_SOURCE_CONSTRUCTOR)
-    public AttributionSource(int uid, int pid, @Nullable String packageName,
-            @Nullable String attributionTag, @NonNull IBinder token,
-            @Nullable String[] renouncedPermissions,
-            @Nullable AttributionSource next) {
-        this(uid, pid, packageName, attributionTag, token, renouncedPermissions,
-                Context.DEVICE_ID_DEFAULT, next);
-    }
-
-    /** @hide */
-    @TestApi
     @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
     public AttributionSource(int uid, int pid, @Nullable String packageName,
             @Nullable String attributionTag, @NonNull IBinder token,
@@ -473,6 +462,20 @@
     }
 
     /**
+     * @return The next package's device Id from its context.
+     * This device ID is used for permissions checking during attribution source validation.
+     *
+     * @hide
+     */
+    public int getNextDeviceId() {
+        if (mAttributionSourceState.next != null
+                && mAttributionSourceState.next.length > 0) {
+            return mAttributionSourceState.next[0].deviceId;
+        }
+        return Context.DEVICE_ID_DEFAULT;
+    }
+
+    /**
      * Checks whether this attribution source can be trusted. That is whether
      * the app it refers to created it and provided to the attribution chain.
      *
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index be40143..cd3ce87 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -1492,12 +1492,12 @@
         /**
          * Sets which surfaces a shortcut will be excluded from.
          *
-         * If the shortcut is set to be excluded from {@link #SURFACE_LAUNCHER}, shortcuts will be
-         * excluded from the search result of {@link android.content.pm.LauncherApps#getShortcuts(
-         * android.content.pm.LauncherApps.ShortcutQuery, UserHandle)} nor
-         * {@link android.content.pm.ShortcutManager#getShortcuts(int)}. This generally means the
-         * shortcut would not be displayed by a launcher app (e.g. in Long-Press menu), while
-         * remain visible in other surfaces such as assistant or on-device-intelligence.
+         * This API is reserved for future extension. Currently, marking a shortcut to be
+         * excluded from {@link #SURFACE_LAUNCHER} will not publish the shortcut, thus
+         * the following operations will be a no-op:
+         * {@link android.content.pm.ShortcutManager#pushDynamicShortcut(android.content.pm.ShortcutInfo)},
+         * {@link android.content.pm.ShortcutManager#addDynamicShortcuts(List)}, and
+         * {@link android.content.pm.ShortcutManager#setDynamicShortcuts(List)}.
          */
         @NonNull
         public Builder setExcludedFromSurfaces(final int surfaces) {
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 45591d7..cee8d96 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -137,6 +137,18 @@
 }
 
 flag {
+    name: "get_package_storage_stats"
+    namespace: "system_performance"
+    is_exported: true
+    description: "Add dumpsys entry point for package StorageStats"
+    bug: "332905331"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "provide_info_of_apk_in_apex"
     is_exported: true
     namespace: "package_manager_service"
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index cd1913b..83742eb 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -240,3 +240,10 @@
     description: "Add entrypoint in Settings Reset options for deleting private space when lock is forgotten"
     bug: "329601751"
 }
+
+flag {
+    name: "allow_main_user_to_access_blocked_number_provider"
+    namespace: "multiuser"
+    description: "Allow MAIN user to access blocked number provider"
+    bug: "338579331"
+}
diff --git a/core/java/android/credentials/GetCandidateCredentialsResponse.java b/core/java/android/credentials/GetCandidateCredentialsResponse.java
index 3d8ccaa..c70eff4 100644
--- a/core/java/android/credentials/GetCandidateCredentialsResponse.java
+++ b/core/java/android/credentials/GetCandidateCredentialsResponse.java
@@ -18,6 +18,8 @@
 
 import android.annotation.Hide;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.credentials.selection.GetCredentialProviderData;
 import android.os.Parcel;
@@ -39,6 +41,9 @@
     @NonNull
     private final List<GetCredentialProviderData> mCandidateProviderDataList;
 
+    @Nullable
+    private final ComponentName mPrimaryProviderComponentName;
+
     @NonNull
     private final Intent mIntent;
 
@@ -48,13 +53,15 @@
     @Hide
     public GetCandidateCredentialsResponse(
             @NonNull List<GetCredentialProviderData> candidateProviderDataList,
-            @NonNull Intent intent
+            @NonNull Intent intent,
+            @Nullable ComponentName primaryProviderComponentName
     ) {
         Preconditions.checkCollectionNotEmpty(
                 candidateProviderDataList,
                 /*valueName=*/ "candidateProviderDataList");
         mCandidateProviderDataList = new ArrayList<>(candidateProviderDataList);
         mIntent = intent;
+        mPrimaryProviderComponentName = primaryProviderComponentName;
     }
 
     /**
@@ -67,6 +74,16 @@
     }
 
     /**
+     * Returns the primary provider component name.
+     *
+     * @hide
+     */
+    @Nullable
+    public ComponentName getPrimaryProviderComponentName() {
+        return mPrimaryProviderComponentName;
+    }
+
+    /**
      * Returns candidate provider data list.
      *
      * @hide
@@ -83,12 +100,15 @@
 
         AnnotationValidations.validate(NonNull.class, null, mCandidateProviderDataList);
         mIntent = in.readTypedObject(Intent.CREATOR);
+
+        mPrimaryProviderComponentName = in.readTypedObject(ComponentName.CREATOR);
     }
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeTypedList(mCandidateProviderDataList);
         dest.writeTypedObject(mIntent, flags);
+        dest.writeTypedObject(mPrimaryProviderComponentName, flags);
     }
 
     @Override
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 6d9b51cb..2e1e90c 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -200,6 +200,8 @@
                 supportedCaptureSizes.put(format, supportedSizes);
             }
         }
+
+        int captureFormat = ImageFormat.UNKNOWN;
         Surface burstCaptureSurface = CameraExtensionUtils.getBurstCaptureSurface(
                 config.getOutputConfigurations(), supportedCaptureSizes);
         OutputConfiguration burstCaptureOutputConfig = null;
@@ -210,6 +212,12 @@
                 }
             }
             suitableSurfaceCount++;
+
+            if (Flags.analytics24q3()) {
+                CameraExtensionUtils.SurfaceInfo burstCaptureSurfaceInfo =
+                        CameraExtensionUtils.querySurface(burstCaptureSurface);
+                captureFormat = burstCaptureSurfaceInfo.mFormat;
+            }
         }
 
         if (suitableSurfaceCount != config.getOutputConfigurations().size()) {
@@ -249,6 +257,9 @@
                 burstCaptureOutputConfig, postviewOutputConfig, config.getStateCallback(),
                 config.getExecutor(), sessionId, token, config.getExtension());
 
+        if (Flags.analytics24q3()) {
+            ret.mStatsAggregator.setCaptureFormat(captureFormat);
+        }
         ret.mStatsAggregator.setClientName(ctx.getOpPackageName());
         ret.mStatsAggregator.setExtensionType(config.getExtension());
 
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 3ae3199..a4ae398 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -200,10 +200,18 @@
                 supportedCaptureSizes.put(format, supportedSizes);
             }
         }
+
+        int captureFormat = ImageFormat.UNKNOWN;
         Surface burstCaptureSurface = CameraExtensionUtils.getBurstCaptureSurface(
                 config.getOutputConfigurations(), supportedCaptureSizes);
         if (burstCaptureSurface != null) {
             suitableSurfaceCount++;
+
+            if (Flags.analytics24q3()) {
+                CameraExtensionUtils.SurfaceInfo burstCaptureSurfaceInfo =
+                        CameraExtensionUtils.querySurface(burstCaptureSurface);
+                captureFormat = burstCaptureSurfaceInfo.mFormat;
+            }
         }
 
         if (suitableSurfaceCount != config.getOutputConfigurations().size()) {
@@ -258,6 +266,9 @@
                 extensionChars.getAvailableCaptureResultKeys(config.getExtension()),
                 config.getExtension());
 
+        if (Flags.analytics24q3()) {
+            session.mStatsAggregator.setCaptureFormat(captureFormat);
+        }
         session.mStatsAggregator.setClientName(ctx.getOpPackageName());
         session.mStatsAggregator.setExtensionType(config.getExtension());
 
diff --git a/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java b/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java
index 3050a51..c75e418 100644
--- a/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java
+++ b/core/java/android/hardware/camera2/utils/ExtensionSessionStatsAggregator.java
@@ -70,6 +70,23 @@
     }
 
     /**
+     * Set the capture format.
+     *
+     * @param format Format of requested capture.
+     */
+    public void setCaptureFormat(int format) {
+        synchronized (mLock) {
+            if (mIsDone) {
+                return;
+            }
+            if (DEBUG) {
+                Log.v(TAG, "Setting capture format: " + format);
+            }
+            mStats.captureFormat = format;
+        }
+    }
+
+    /**
      * Set extension type.
      *
      * @param extensionType Type of extension. Must match one of
@@ -116,7 +133,8 @@
                 + "  cameraId: '" + stats.cameraId + "'\n"
                 + "  clientName: '" + stats.clientName + "'\n"
                 + "  type: '" + stats.type + "'\n"
-                + "  isAdvanced: '" + stats.isAdvanced + "'\n";
+                + "  isAdvanced: '" + stats.isAdvanced + "'\n"
+                + "  captureFormat: '" + stats.captureFormat + "'\n";
     }
 
     /**
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index ac043d3..91b05c2 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -1353,9 +1353,6 @@
     /**
      * Get a snapshot of the real-time status of the devices on the CEC bus.
      *
-     * <p>This only applies to devices with switch functionality, which are devices with one
-     * or more than one HDMI inputs.
-     *
      * @return a list of {@link HdmiDeviceInfo} of the connected CEC devices on the CEC bus. An
      * empty list will be returned if there is none.
      */
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 243ae14..45b316a 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -148,8 +148,6 @@
 
     IInputDeviceBatteryState getBatteryState(int deviceId);
 
-    void setPointerIconType(int typeId);
-    void setCustomPointerIcon(in PointerIcon icon);
     boolean setPointerIcon(in PointerIcon icon, int displayId, int deviceId, int pointerId,
             in IBinder inputToken);
 
@@ -173,9 +171,9 @@
     void removeUniqueIdAssociationByDescriptor(in String inputDeviceDescriptor);
 
     // Add a runtime association between the input device and display, using device's port.
-    void addUniqueIdAssociation(in String inputPort, in String displayUniqueId);
+    void addUniqueIdAssociationByPort(in String inputPort, in String displayUniqueId);
     // Remove the runtime association between the input device and display, using device's port.
-    void removeUniqueIdAssociation(in String inputPort);
+    void removeUniqueIdAssociationByPort(in String inputPort);
 
     InputSensorInfo[] getSensorList(int deviceId);
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index dd4ea31..7527aa7 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -992,21 +992,14 @@
     }
 
     /**
-     * Changes the mouse pointer's icon shape into the specified id.
+     * This method exists for backwards-compatibility, and is a no-op.
      *
-     * @param iconId The id of the pointer graphic, as a value between
-     * {@link PointerIcon#TYPE_ARROW} and {@link PointerIcon#TYPE_HANDWRITING}.
-     *
+     * @deprecated
      * @hide
      */
     @UnsupportedAppUsage
     public void setPointerIconType(int iconId) {
-        mGlobal.setPointerIconType(iconId);
-    }
-
-    /** @hide */
-    public void setCustomPointerIcon(PointerIcon icon) {
-        mGlobal.setCustomPointerIcon(icon);
+        Log.e(TAG, "setPointerIcon: Unsupported app usage!");
     }
 
     /** @hide */
@@ -1101,10 +1094,9 @@
      */
     @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
     @TestApi
-    // TODO(b/324075859): Rename to addUniqueIdAssociationByPort
-    public void addUniqueIdAssociation(@NonNull String inputPort,
+    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
             @NonNull String displayUniqueId) {
-        mGlobal.addUniqueIdAssociation(inputPort, displayUniqueId);
+        mGlobal.addUniqueIdAssociationByPort(inputPort, displayUniqueId);
     }
 
     /**
@@ -1117,9 +1109,8 @@
      */
     @RequiresPermission(android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY)
     @TestApi
-    // TODO(b/324075859): Rename to removeUniqueIdAssociationByPort
-    public void removeUniqueIdAssociation(@NonNull String inputPort) {
-        mGlobal.removeUniqueIdAssociation(inputPort);
+    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
+        mGlobal.removeUniqueIdAssociationByPort(inputPort);
     }
 
     /**
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index a9c97b1..fcd5a3e 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1411,28 +1411,6 @@
     }
 
     /**
-     * @see InputManager#setPointerIconType(int)
-     */
-    public void setPointerIconType(int iconId) {
-        try {
-            mIm.setPointerIconType(iconId);
-        } catch (RemoteException ex) {
-            throw ex.rethrowFromSystemServer();
-        }
-    }
-
-    /**
-     * @see InputManager#setCustomPointerIcon(PointerIcon)
-     */
-    public void setCustomPointerIcon(PointerIcon icon) {
-        try {
-            mIm.setCustomPointerIcon(icon);
-        } catch (RemoteException ex) {
-            throw ex.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * @see InputManager#setPointerIcon(PointerIcon, int, int, int, IBinder)
      */
     public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId,
@@ -1467,22 +1445,23 @@
     }
 
     /**
-     * @see InputManager#addUniqueIdAssociation(String, String)
+     * @see InputManager#addUniqueIdAssociationByPort(String, String)
      */
-    public void addUniqueIdAssociation(@NonNull String inputPort, @NonNull String displayUniqueId) {
+    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
+            @NonNull String displayUniqueId) {
         try {
-            mIm.addUniqueIdAssociation(inputPort, displayUniqueId);
+            mIm.addUniqueIdAssociationByPort(inputPort, displayUniqueId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * @see InputManager#removeUniqueIdAssociation(String)
+     * @see InputManager#removeUniqueIdAssociationByPort(String)
      */
-    public void removeUniqueIdAssociation(@NonNull String inputPort) {
+    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
         try {
-            mIm.removeUniqueIdAssociation(inputPort);
+            mIm.removeUniqueIdAssociationByPort(inputPort);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index 9fe0bef..d4d1ed2 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -38,6 +38,16 @@
 }
 
 flag{
+    name: "enforce_main_user"
+    namespace: "vcn"
+    description: "Enforce main user to make VCN HSUM compatible"
+    bug: "310310661"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag{
     name: "handle_seq_num_leap"
     namespace: "vcn"
     description: "Do not report bad network when there is a suspected sequence number leap"
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index e057a85..360b2ac 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -18,6 +18,7 @@
 package android.os;
 
 import android.os.IHintSession;
+import android.hardware.power.ChannelConfig;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
 
@@ -27,6 +28,9 @@
      * Creates a {@link Session} for the given set of threads and associates to a binder token.
      * Returns a config if creation is not supported, and HMS had to use the
      * legacy creation method.
+     *
+     * Throws UnsupportedOperationException if ADPF is not supported, and IllegalStateException
+     * if creation is supported but fails.
      */
     IHintSession createHintSessionWithConfig(in IBinder token, in int[] threadIds,
             in long durationNanos, in SessionTag tag, out @nullable SessionConfig config);
@@ -38,4 +42,12 @@
 
     void setHintSessionThreads(in IHintSession hintSession, in int[] tids);
     int[] getHintSessionThreadIds(in IHintSession hintSession);
+
+    /**
+     * Returns FMQ channel information for the caller, which it associates to a binder token.
+     *
+     * Throws IllegalStateException if FMQ channel creation fails.
+     */
+    ChannelConfig getSessionChannel(in IBinder token);
+    oneway void closeSessionChannel();
 }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index c6a9203..2f0d634 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1930,12 +1930,10 @@
     public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
 
     /**
-     * This user restriction specifies if the user is able to add SIMs to the device.
+     * This user restriction specifies if the user is able to add embedded SIMs to the device.
      *
      * <p>
-     * This restriction blocks the download of embedded SIMs, and disables any physical SIMs.
-     * If any embedded SIMs are already on the device, then they are removed. This restriction
-     * does not affect SIMs provisioned to the device by device owners or profile owners.
+     * This restriction blocks the download of embedded SIMs.
      *
      * <p>
      * This restriction can only be set by a device owner or a profile owner of an
@@ -1951,6 +1949,7 @@
      *
      * <p>Key for user restrictions.
      * <p>Type: Boolean
+     *
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
      * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
      * @see #getUserRestrictions()
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index b588308..0e28560 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -44,14 +44,6 @@
 }
 
 flag {
-  name: "attribution_source_constructor"
-  is_exported: true
-  namespace: "permissions"
-  description: "enable AttributionSource(int, int, String, String, IBinder, String[], AttributionSource)"
-  bug: "304478648"
-}
-
-flag {
     name: "enhanced_confirmation_mode_apis_enabled"
     is_exported: true
     is_fixed_read_only: true
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e6ddf35..009713f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5123,13 +5123,6 @@
         public static final String SCREEN_BRIGHTNESS = "screen_brightness";
 
         /**
-         * The screen backlight brightness between 0.0f and 1.0f.
-         * @hide
-         */
-        @Readable
-        public static final String SCREEN_BRIGHTNESS_FLOAT = "screen_brightness_float";
-
-        /**
          * Control whether to enable automatic brightness mode.
          */
         @Readable
@@ -6273,7 +6266,6 @@
             PUBLIC_SETTINGS.add(DIM_SCREEN);
             PUBLIC_SETTINGS.add(SCREEN_OFF_TIMEOUT);
             PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS);
-            PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS_FLOAT);
             PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS_MODE);
             PUBLIC_SETTINGS.add(MODE_RINGER_STREAMS_AFFECTED);
             PUBLIC_SETTINGS.add(MUTE_STREAMS_AFFECTED);
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 51758aa..ee5e533 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -81,8 +81,15 @@
 }
 
 flag {
-  name: "report_primary_auth_attempts"
-  namespace: "biometrics"
-  description: "Report primary auth attempts from LockSettingsService"
-  bug: "285053096"
+    name: "report_primary_auth_attempts"
+    namespace: "biometrics"
+    description: "Report primary auth attempts from LockSettingsService"
+    bug: "285053096"
+}
+
+flag {
+    name: "dump_attestation_verifications"
+    namespace: "hardware_backed_security"
+    description: "Add a dump capability for attestation_verification service"
+    bug: "335498868"
 }
diff --git a/core/java/android/service/credentials/CredentialProviderInfoFactory.java b/core/java/android/service/credentials/CredentialProviderInfoFactory.java
index 92f2c32..3cd705a 100644
--- a/core/java/android/service/credentials/CredentialProviderInfoFactory.java
+++ b/core/java/android/service/credentials/CredentialProviderInfoFactory.java
@@ -86,7 +86,8 @@
             @NonNull Context context,
             @NonNull ComponentName serviceComponent,
             int userId,
-            boolean isSystemProvider)
+            boolean isSystemProvider,
+            boolean isPrimary)
             throws PackageManager.NameNotFoundException {
         return create(
                 context,
@@ -94,7 +95,7 @@
                 isSystemProvider,
                 /* disableSystemAppVerificationForTests= */ false,
                 /* isEnabled= */ false,
-                /* isPrimary= */ false);
+                isPrimary);
     }
 
     /**
diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java
index db665a9..c4becea 100644
--- a/core/java/android/view/DisplayCutout.java
+++ b/core/java/android/view/DisplayCutout.java
@@ -1392,10 +1392,6 @@
 
     private static Rect computeSafeInsets(int displayW, int displayH, Insets waterFallInsets,
             Rect[] bounds) {
-        if (displayW == displayH) {
-            throw new UnsupportedOperationException("not implemented: display=" + displayW + "x"
-                    + displayH + " bounding rects=" + Arrays.toString(bounds));
-        }
 
         int leftInset = Math.max(waterFallInsets.left, findCutoutInsetForSide(
                 displayW, displayH, bounds[BOUNDS_POSITION_LEFT], Gravity.LEFT));
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 1c0834f..3743035 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -108,11 +108,6 @@
     void dispatchDragEvent(in DragEvent event);
 
     /**
-     * Pointer icon events
-     */
-    void updatePointerIcon(float x, float y);
-
-    /**
      * Called for non-application windows when the enter animation has completed.
      */
     void dispatchWindowShown();
@@ -128,4 +123,9 @@
      * @param callbacks to receive responses
      */
     void requestScrollCapture(in IScrollCaptureResponseListener callbacks);
+
+    /**
+     * Dump the details of a window.
+     */
+    void dumpWindow(in ParcelFileDescriptor pfd);
 }
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 86264eb..e3e4fc0 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -288,8 +288,6 @@
 
     oneway void finishMovingTask(IWindow window);
 
-    oneway void updatePointerIcon(IWindow window);
-
     /**
      * Update a tap exclude region identified by provided id in the window. Touches on this region
      * will neither be dispatched to this window nor change the focus to this window. Passing an
diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java
index 4530157..9503f49 100644
--- a/core/java/android/view/ImeBackAnimationController.java
+++ b/core/java/android/view/ImeBackAnimationController.java
@@ -38,6 +38,8 @@
 
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 
+import java.io.PrintWriter;
+
 /**
  * Controller for IME predictive back animation
  *
@@ -271,4 +273,24 @@
         return mPostCommitAnimator != null && mTriggerBack;
     }
 
+    /**
+     * Dump information about this ImeBackAnimationController
+     *
+     * @param prefix the prefix that will be prepended to each line of the produced output
+     * @param writer the writer that will receive the resulting text
+     */
+    public void dump(String prefix, PrintWriter writer) {
+        final String innerPrefix = prefix + "    ";
+        writer.println(prefix + "ImeBackAnimationController:");
+        writer.println(innerPrefix + "mLastProgress=" + mLastProgress);
+        writer.println(innerPrefix + "mTriggerBack=" + mTriggerBack);
+        writer.println(innerPrefix + "mIsPreCommitAnimationInProgress="
+                + mIsPreCommitAnimationInProgress);
+        writer.println(innerPrefix + "mStartRootScrollY=" + mStartRootScrollY);
+        writer.println(innerPrefix + "isBackAnimationAllowed=" + isBackAnimationAllowed());
+        writer.println(innerPrefix + "isAdjustPan=" + isAdjustPan());
+        writer.println(innerPrefix + "isHideAnimationInProgress="
+                + isHideAnimationInProgress());
+    }
+
 }
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index c545267..f1cb410 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1799,8 +1799,11 @@
     }
 
     void dump(String prefix, PrintWriter pw) {
-        pw.print(prefix); pw.println("InsetsController:");
-        mState.dump(prefix + "  ", pw);
+        final String innerPrefix = prefix + "    ";
+        pw.println(prefix + "InsetsController:");
+        mState.dump(innerPrefix, pw);
+        pw.println(innerPrefix + "mIsPredictiveBackImeHideAnimInProgress="
+                + mIsPredictiveBackImeHideAnimInProgress);
     }
 
     void dumpDebug(ProtoOutputStream proto, long fieldId) {
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index a23df79..1d70d18 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -986,7 +986,7 @@
 
             updateBackgroundVisibility(surfaceUpdateTransaction);
             updateBackgroundColor(surfaceUpdateTransaction);
-            if (mLimitedHdrEnabled && hdrHeadroomChanged) {
+            if (mLimitedHdrEnabled && (hdrHeadroomChanged || creating)) {
                 surfaceUpdateTransaction.setDesiredHdrHeadroom(
                         mBlastSurfaceControl, mHdrHeadroom);
             }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 60ad926..1cb2765 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30695,21 +30695,11 @@
      */
     public void setPointerIcon(PointerIcon pointerIcon) {
         mMousePointerIcon = pointerIcon;
-        if (com.android.input.flags.Flags.enablePointerChoreographer()) {
-            final ViewRootImpl viewRootImpl = getViewRootImpl();
-            if (viewRootImpl == null) {
-                return;
-            }
-            viewRootImpl.refreshPointerIcon();
-        } else {
-            if (mAttachInfo == null || mAttachInfo.mHandlingPointerEvent) {
-                return;
-            }
-            try {
-                mAttachInfo.mSession.updatePointerIcon(mAttachInfo.mWindow);
-            } catch (RemoteException e) {
-            }
+        final ViewRootImpl viewRootImpl = getViewRootImpl();
+        if (viewRootImpl == null) {
+            return;
         }
+        viewRootImpl.refreshPointerIcon();
     }
 
     /**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index db55a9d..155c053 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -74,7 +74,6 @@
 import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES;
 import static android.view.ViewRootImplProto.WIN_FRAME;
 import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
@@ -96,6 +95,7 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
@@ -122,7 +122,6 @@
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
 
-import static com.android.input.flags.Flags.enablePointerChoreographer;
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.window.flags.Flags.activityWindowInfoFlag;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
@@ -195,6 +194,7 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.StrictMode;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.Trace;
@@ -268,11 +268,15 @@
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.policy.PhoneFallbackEventHandler;
+import com.android.internal.util.FastPrintWriter;
 import com.android.internal.view.BaseSurfaceHolder;
 import com.android.internal.view.RootViewSurfaceTaker;
 import com.android.internal.view.SurfaceCallbackHelper;
 import com.android.modules.expresslog.Counter;
 
+import libcore.io.IoUtils;
+
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
@@ -423,6 +427,12 @@
 
     private static final long NANOS_PER_SEC = 1000000000;
 
+    // If the ViewRootImpl has been idle for more than 200ms, clear the preferred
+    // frame rate category and frame rate.
+    private static final int IDLE_TIME_MILLIS = 250;
+
+    private static final long NANOS_PER_MILLI = 1_000_000;
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
 
@@ -655,6 +665,8 @@
     private int mMinusOneFrameIntervalMillis = 0;
     // VRR interval between the previous and the frame before
     private int mMinusTwoFrameIntervalMillis = 0;
+    // VRR has the invalidation idle message been posted?
+    private boolean mInvalidationIdleMessagePosted = false;
 
     /**
      * Update the Choreographer's FrameInfo object with the timing information for the current
@@ -4266,6 +4278,10 @@
         // when the values are applicable.
         if (mDrawnThisFrame) {
             mDrawnThisFrame = false;
+            if (!mInvalidationIdleMessagePosted) {
+                mInvalidationIdleMessagePosted = true;
+                mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS);
+            }
             setCategoryFromCategoryCounts();
             updateInfrequentCount();
             setPreferredFrameRate(mPreferredFrameRate);
@@ -5300,10 +5316,12 @@
         if (DEBUG_CONTENT_CAPTURE) {
             Log.v(mTag, "performContentCaptureInitialReport() on " + rootView);
         }
+        boolean traceDispatchCapture = false;
         try {
             if (!isContentCaptureEnabled()) return;
 
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+            traceDispatchCapture = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+            if (traceDispatchCapture) {
                 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchContentCapture() for "
                         + getClass().getSimpleName());
             }
@@ -5319,7 +5337,9 @@
             // Content capture is a go!
             rootView.dispatchInitialProvideContentCaptureStructure();
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            if (traceDispatchCapture) {
+                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            }
         }
     }
 
@@ -5327,10 +5347,12 @@
         if (DEBUG_CONTENT_CAPTURE) {
             Log.v(mTag, "handleContentCaptureFlush()");
         }
+        boolean traceFlushContentCapture = false;
         try {
             if (!isContentCaptureEnabled()) return;
 
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+            traceFlushContentCapture = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+            if (traceFlushContentCapture) {
                 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "flushContentCapture for "
                         + getClass().getSimpleName());
             }
@@ -5342,7 +5364,9 @@
             }
             ccm.flush(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            if (traceFlushContentCapture) {
+                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            }
         }
     }
 
@@ -6499,6 +6523,8 @@
                     return "MSG_WINDOW_TOUCH_MODE_CHANGED";
                 case MSG_KEEP_CLEAR_RECTS_CHANGED:
                     return "MSG_KEEP_CLEAR_RECTS_CHANGED";
+                case MSG_CHECK_INVALIDATION_IDLE:
+                    return "MSG_CHECK_INVALIDATION_IDLE";
                 case MSG_REFRESH_POINTER_ICON:
                     return "MSG_REFRESH_POINTER_ICON";
                 case MSG_TOUCH_BOOST_TIMEOUT:
@@ -6763,6 +6789,30 @@
                     mNumPausedForSync = 0;
                     scheduleTraversals();
                     break;
+                case MSG_CHECK_INVALIDATION_IDLE: {
+                    long delta;
+                    if (mIsTouchBoosting || mIsFrameRateBoosting || mInsetsAnimationRunning) {
+                        delta = 0;
+                    } else {
+                        delta = System.nanoTime() / NANOS_PER_MILLI - mLastUpdateTimeMillis;
+                    }
+                    if (delta >= IDLE_TIME_MILLIS) {
+                        mFrameRateCategoryHighCount = 0;
+                        mFrameRateCategoryHighHintCount = 0;
+                        mFrameRateCategoryNormalCount = 0;
+                        mFrameRateCategoryLowCount = 0;
+                        mPreferredFrameRate = 0;
+                        mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+                        setPreferredFrameRateCategory(FRAME_RATE_CATEGORY_NO_PREFERENCE);
+                        setPreferredFrameRate(0f);
+                        mInvalidationIdleMessagePosted = false;
+                    } else {
+                        mInvalidationIdleMessagePosted = true;
+                        mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE,
+                                IDLE_TIME_MILLIS - delta);
+                    }
+                    break;
+                }
                 case MSG_TOUCH_BOOST_TIMEOUT:
                     /**
                      * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
@@ -7929,46 +7979,20 @@
         if (event.isStylusPointer() && mIsStylusPointerIconEnabled) {
             pointerIcon = mHandwritingInitiator.onResolvePointerIcon(mContext, event);
         }
-
         if (pointerIcon == null) {
             pointerIcon = mView.onResolvePointerIcon(event, pointerIndex);
         }
-
-        if (enablePointerChoreographer()) {
-            if (pointerIcon == null) {
-                pointerIcon = PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED);
-            }
-            if (Objects.equals(mResolvedPointerIcon, pointerIcon)) {
-                return true;
-            }
-            mResolvedPointerIcon = pointerIcon;
-
-            InputManagerGlobal.getInstance()
-                    .setPointerIcon(pointerIcon, event.getDisplayId(),
-                            event.getDeviceId(), event.getPointerId(0), getInputToken());
+        if (pointerIcon == null) {
+            pointerIcon = PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED);
+        }
+        if (Objects.equals(mResolvedPointerIcon, pointerIcon)) {
             return true;
         }
+        mResolvedPointerIcon = pointerIcon;
 
-        final int pointerType = (pointerIcon != null) ?
-                pointerIcon.getType() : PointerIcon.TYPE_NOT_SPECIFIED;
-
-        if (mPointerIconType == null || mPointerIconType != pointerType) {
-            mPointerIconType = pointerType;
-            mCustomPointerIcon = null;
-            if (mPointerIconType != PointerIcon.TYPE_CUSTOM) {
-                InputManagerGlobal
-                        .getInstance()
-                        .setPointerIconType(pointerType);
-                return true;
-            }
-        }
-        if (mPointerIconType == PointerIcon.TYPE_CUSTOM &&
-                !pointerIcon.equals(mCustomPointerIcon)) {
-            mCustomPointerIcon = pointerIcon;
-            InputManagerGlobal
-                    .getInstance()
-                    .setCustomPointerIcon(mCustomPointerIcon);
-        }
+        InputManagerGlobal.getInstance()
+                .setPointerIcon(pointerIcon, event.getDisplayId(),
+                        event.getDeviceId(), event.getPointerId(0), getInputToken());
         return true;
     }
 
@@ -9601,6 +9625,8 @@
 
         mOnBackInvokedDispatcher.dump(prefix, writer);
 
+        mImeBackAnimationController.dump(prefix, writer);
+
         writer.println(prefix + "View Hierarchy:");
         dumpViewHierarchy(innerPrefix, writer, mView);
     }
@@ -10570,16 +10596,6 @@
         mHandler.sendMessage(msg);
     }
 
-    public void updatePointerIcon(float x, float y) {
-        final int what = MSG_UPDATE_POINTER_ICON;
-        mHandler.removeMessages(what);
-        final long now = SystemClock.uptimeMillis();
-        final MotionEvent event = MotionEvent.obtain(
-                0, now, MotionEvent.ACTION_HOVER_MOVE, x, y, 0);
-        Message msg = mHandler.obtainMessage(what, event);
-        mHandler.sendMessage(msg);
-    }
-
     public void dispatchCheckFocus() {
         if (!mHandler.hasMessages(MSG_CHECK_FOCUS)) {
             // This will result in a call to checkFocus() below.
@@ -11443,14 +11459,6 @@
         }
 
         @Override
-        public void updatePointerIcon(float x, float y) {
-            final ViewRootImpl viewAncestor = mViewAncestor.get();
-            if (viewAncestor != null) {
-                viewAncestor.updatePointerIcon(x, y);
-            }
-        }
-
-        @Override
         public void dispatchWindowShown() {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (viewAncestor != null) {
@@ -11473,6 +11481,26 @@
                 viewAncestor.dispatchScrollCaptureRequest(listener);
             }
         }
+
+        @Override
+        public void dumpWindow(ParcelFileDescriptor pfd) {
+            final ViewRootImpl viewAncestor = mViewAncestor.get();
+            if (viewAncestor == null) {
+                return;
+            }
+            viewAncestor.mHandler.postAtFrontOfQueue(() -> {
+                final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+                try {
+                    PrintWriter pw = new FastPrintWriter(new FileOutputStream(
+                            pfd.getFileDescriptor()));
+                    viewAncestor.dump("", pw);
+                    pw.flush();
+                } finally {
+                    IoUtils.closeQuietly(pfd);
+                    StrictMode.setThreadPolicy(oldPolicy);
+                }
+            });
+        }
     }
 
     public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
@@ -12657,10 +12685,12 @@
             view = mFrameRateCategoryView;
         }
 
+        boolean traceFrameRateCategory = false;
         try {
             if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT
                     && mLastPreferredFrameRateCategory != frameRateCategory) {
-                if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                traceFrameRateCategory = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+                if (traceFrameRateCategory) {
                     String reason = reasonToString(frameRateReason);
                     String sourceView = view == null ? "-" : view;
                     String category = categoryToString(frameRateCategory);
@@ -12678,7 +12708,9 @@
         } catch (Exception e) {
             Log.e(mTag, "Unable to set frame rate category", e);
         } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            if (traceFrameRateCategory) {
+                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            }
         }
     }
 
@@ -13002,6 +13034,10 @@
     private void removeVrrMessages() {
         mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT);
         mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+        if (mInvalidationIdleMessagePosted) {
+            mInvalidationIdleMessagePosted = false;
+            mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE);
+        }
     }
 
     /**
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index e6367ff..d7d764b 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -603,10 +603,6 @@
     }
 
     @Override
-    public void updatePointerIcon(android.view.IWindow window) {
-    }
-
-    @Override
     public void updateTapExcludeRegion(android.view.IWindow window,
             android.graphics.Region region) {
     }
diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java
index a2f3544..5aaa994 100644
--- a/core/java/android/view/animation/AnimationSet.java
+++ b/core/java/android/view/animation/AnimationSet.java
@@ -374,7 +374,7 @@
             final Animation a = mAnimations.get(i);
 
             temp.clear();
-            a.getTransformationAt(interpolatedTime, t);
+            a.getTransformationAt(interpolatedTime, temp);
             t.compose(temp);
         }
     }
diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java
index de31667..812ecd1 100644
--- a/core/java/android/view/animation/Transformation.java
+++ b/core/java/android/view/animation/Transformation.java
@@ -78,6 +78,7 @@
         mHasClipRect = false;
         mAlpha = 1.0f;
         mTransformationType = TYPE_BOTH;
+        mInsets = Insets.NONE;
     }
 
     /**
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index bfe4e6f..9cc4191 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -1978,6 +1978,13 @@
 
                     if (Objects.equals(mLastAutofilledData.get(id), value)) {
                         view.setAutofilled(true, hideHighlight);
+                        try {
+                            mService.setViewAutofilled(mSessionId, id, mContext.getUserId());
+                        } catch (RemoteException e) {
+                            // The failure could be a consequence of something going wrong on the
+                            // server side. Do nothing here since it's just logging, but it's
+                            // possible follow-up actions may fail.
+                        }
                     } else {
                         view.setAutofilled(false, false);
                         mLastAutofilledData.remove(id);
@@ -2978,6 +2985,13 @@
                 mLastAutofilledData.put(view.getAutofillId(), targetValue);
             }
             view.setAutofilled(true, hideHighlight);
+            try {
+                mService.setViewAutofilled(mSessionId, view.getAutofillId(), mContext.getUserId());
+            } catch (RemoteException e) {
+                // The failure could be a consequence of something going wrong on the server side.
+                // Do nothing here since it's just logging, but it's possible follow-up actions may
+                // fail.
+            }
         }
     }
 
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index 1a9322e..2039b4d 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -49,6 +49,7 @@
     void updateSession(int sessionId, in AutofillId id, in Rect bounds,
         in AutofillValue value, int action, int flags, int userId);
     void setAutofillFailure(int sessionId, in List<AutofillId> ids, int userId);
+    void setViewAutofilled(int sessionId, in AutofillId id, int userId);
     void finishSession(int sessionId, int userId, int commitReason);
     void cancelSession(int sessionId, int userId);
     void setAuthenticationResult(in Bundle data, int sessionId, int authenticationId, int userId);
diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java
index c5114b9..fb3e083 100644
--- a/core/java/android/view/inputmethod/EditorInfo.java
+++ b/core/java/android/view/inputmethod/EditorInfo.java
@@ -720,6 +720,20 @@
 
     private boolean mIsStylusHandwritingEnabled;
 
+
+    /**
+     * AndroidX Core library 1.13.0 introduced EditorInfoCompat#setStylusHandwritingEnabled and
+     * EditorInfoCompat#isStylusHandwritingEnabled which used a boolean value in the EditorInfo
+     * extras bundle. These methods do not set or check the Android V property since the Android V
+     * SDK was not yet available. In order for EditorInfoCompat#isStylusHandwritingEnabled to return
+     * the correct value for EditorInfo created by Android V TextView, the extras bundle value
+     * should be set. This is the extras bundle key.
+     *
+     * @hide
+     */
+    public static final String STYLUS_HANDWRITING_ENABLED_ANDROIDX_EXTRAS_KEY =
+            "androidx.core.view.inputmethod.EditorInfoCompat.STYLUS_HANDWRITING_ENABLED";
+
     /**
      * Set {@code true} if the {@code Editor} has
      * {@link InputMethodManager#startStylusHandwriting stylus handwriting} enabled.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index a073873..cf128fb 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2530,18 +2530,6 @@
                 view, /* delegatorPackageName= */ null, /* handwritingDelegateFlags= */ 0);
     }
 
-    private void startStylusHandwritingInternalAsync(
-            @NonNull View view, @Nullable String delegatorPackageName,
-            @HandwritingDelegateFlags int handwritingDelegateFlags,
-            @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) {
-        Objects.requireNonNull(view);
-        Objects.requireNonNull(executor);
-        Objects.requireNonNull(callback);
-
-        startStylusHandwritingInternal(
-                view, delegatorPackageName, handwritingDelegateFlags, executor, callback);
-    }
-
     private void sendFailureCallback(@NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<Boolean> callback) {
         if (executor == null || callback == null) {
@@ -2891,7 +2879,7 @@
         if (Flags.homeScreenHandwritingDelegator()) {
             flags = delegateView.getHandwritingDelegateFlags();
         }
-        startStylusHandwritingInternalAsync(
+        acceptStylusHandwritingDelegation(
                 delegateView, delegatorPackageName, flags, executor, callback);
     }
 
@@ -2926,6 +2914,9 @@
             @HandwritingDelegateFlags int flags, @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<Boolean> callback) {
         Objects.requireNonNull(delegatorPackageName);
+        Objects.requireNonNull(delegateView);
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
 
         startStylusHandwritingInternal(
                 delegateView, delegatorPackageName, flags, executor, callback);
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index fa9458d..56e5bcf 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -62,6 +62,17 @@
 }
 
 flag {
+    name: "use_input_method_info_safe_list"
+    namespace: "input_method"
+    description: "Use InputMethodInfoSafeList for more reliable binder IPCs"
+    bug: "339761278"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "ime_switcher_revamp"
     is_exported: true
     namespace: "input_method"
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index a07141b..b7ee0b8 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -520,6 +520,13 @@
      * To cancel the request, call <code>filePathCallback.onReceiveValue(null)</code> and
      * return {@code true}.
      *
+     * <p class="note"><b>Note:</b> WebView does not enforce any restrictions on
+     * the chosen file(s). WebView can access all files that your app can access.
+     * In case the file(s) are chosen through an untrusted source such as a third-party
+     * app, it is your own app's responsibility to check what the returned Uris
+     * refer to before calling the <code>filePathCallback</code>. See
+     * {@link #createIntent} and {@link #parseResult} for more details.</p>
+     *
      * @param webView The WebView instance that is initiating the request.
      * @param filePathCallback Invoke this callback to supply the list of paths to files to upload,
      *                         or {@code null} to cancel. Must only be called if the
@@ -556,6 +563,15 @@
          * Parse the result returned by the file picker activity. This method should be used with
          * {@link #createIntent}. Refer to {@link #createIntent} for how to use it.
          *
+         * <p class="note"><b>Note:</b> The intent returned by the file picker activity
+         * should be treated as untrusted. A third-party app handling the implicit
+         * intent created by {@link #createIntent} might return Uris that the third-party
+         * app itself does not have access to, such as your own app's sensitive data files.
+         * WebView does not enforce any restrictions on the returned Uris. It is the
+         * app's responsibility to ensure that the untrusted source (such as a third-party
+         * app) has access the Uris it has returned and that the Uris are not pointing
+         * to any sensitive data files.</p>
+         *
          * @param resultCode the integer result code returned by the file picker activity.
          * @param data the intent returned by the file picker activity.
          * @return the Uris of selected file(s) or {@code null} if the resultCode indicates
@@ -618,6 +634,12 @@
          *   WebChromeClient#onShowFileChooser}</li>
          * </ol>
          *
+         * <p class="note"><b>Note:</b> The created intent may be handled by
+         * third-party applications on device. The received result must be treated
+         * as untrusted as it can contain Uris pointing to your own app's sensitive
+         * data files. Your app should check the resultant Uris in {@link #parseResult}
+         * before calling the <code>filePathCallback</code>.</p>
+         *
          * @return an Intent that supports basic file chooser sources.
          */
         public abstract Intent createIntent();
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index fb1c331..78dd3b1 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -27,6 +27,7 @@
 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
 import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
 import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
+import static android.view.inputmethod.EditorInfo.STYLUS_HANDWRITING_ENABLED_ANDROIDX_EXTRAS_KEY;
 
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
 import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
@@ -10062,9 +10063,22 @@
                 outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
                 outAttrs.setInitialSurroundingText(mText);
                 outAttrs.contentMimeTypes = getReceiveContentMimeTypes();
-                if (android.view.inputmethod.Flags.editorinfoHandwritingEnabled()
-                        && isAutoHandwritingEnabled()) {
-                    outAttrs.setStylusHandwritingEnabled(true);
+                if (android.view.inputmethod.Flags.editorinfoHandwritingEnabled()) {
+                    boolean handwritingEnabled = isAutoHandwritingEnabled();
+                    outAttrs.setStylusHandwritingEnabled(handwritingEnabled);
+                    // AndroidX Core library 1.13.0 introduced
+                    // EditorInfoCompat#setStylusHandwritingEnabled and
+                    // EditorInfoCompat#isStylusHandwritingEnabled which used a boolean value in the
+                    // EditorInfo extras bundle. These methods do not set or check the Android V
+                    // property since the Android V SDK was not yet available. In order for
+                    // EditorInfoCompat#isStylusHandwritingEnabled to return the correct value for
+                    // EditorInfo created by Android V TextView, the extras bundle value is also set
+                    // here.
+                    if (outAttrs.extras == null) {
+                        outAttrs.extras = new Bundle();
+                    }
+                    outAttrs.extras.putBoolean(
+                            STYLUS_HANDWRITING_ENABLED_ANDROIDX_EXTRAS_KEY, handwritingEnabled);
                 }
                 ArrayList<Class<? extends HandwritingGesture>> gestures = new ArrayList<>();
                 gestures.add(SelectGesture.class);
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index e8b4f0b..760c916 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -71,3 +71,10 @@
     description: "Enables quick switch for desktop mode"
     bug: "338066529"
 }
+
+flag {
+    name: "enable_additional_windows_above_status_bar"
+    namespace: "lse_desktop_experience"
+    description: "Allows for additional windows tied to WindowDecoration to be layered between status bar and notification shade."
+    bug: "316186265"
+}
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 33af486..69cac6f 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -49,3 +49,10 @@
     description: "Prevent BAL based on it is bound by foreground Uid but the app switch is stopped."
     bug: "283801068"
 }
+
+flag {
+    name: "bal_improved_metrics"
+    namespace: "responsible_apis"
+    description: "Improved metrics."
+    bug: "339245692"
+}
diff --git a/core/java/com/android/internal/util/NewlineNormalizer.java b/core/java/com/android/internal/util/NewlineNormalizer.java
new file mode 100644
index 0000000..0104d1f
--- /dev/null
+++ b/core/java/com/android/internal/util/NewlineNormalizer.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.internal.util;
+
+
+import java.util.regex.Pattern;
+
+/**
+ * Utility class that replaces consecutive empty lines with single new line.
+ * @hide
+ */
+public class NewlineNormalizer {
+
+    private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?");
+
+    // Private constructor to prevent instantiation
+    private NewlineNormalizer() {}
+
+    /**
+     * Replaces consecutive newlines with a single newline in the input text.
+     */
+    public static String normalizeNewlines(String text) {
+        return MULTIPLE_NEWLINES.matcher(text).replaceAll("\n");
+    }
+}
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index e33704b..3fc4fff 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -18,7 +18,6 @@
 
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.hardware.input.InputManagerGlobal;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -29,7 +28,6 @@
 import android.view.IWindowSession;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
-import android.view.PointerIcon;
 import android.view.ScrollCaptureResponse;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.inputmethod.ImeTracker;
@@ -128,12 +126,6 @@
     }
 
     @Override
-    public void updatePointerIcon(float x, float y) {
-        InputManagerGlobal.getInstance()
-                .setPointerIconType(PointerIcon.TYPE_NOT_SPECIFIED);
-    }
-
-    @Override
     public void dispatchWallpaperCommand(String action, int x, int y,
             int z, Bundle extras, boolean sync) {
         if (sync) {
@@ -162,4 +154,9 @@
             // ignore
         }
     }
+
+    @Override
+    public void dumpWindow(ParcelFileDescriptor pfd) {
+
+    }
 }
diff --git a/core/jni/android_hardware_display_DisplayViewport.cpp b/core/jni/android_hardware_display_DisplayViewport.cpp
index 7f630cb..5d7b33e 100644
--- a/core/jni/android_hardware_display_DisplayViewport.cpp
+++ b/core/jni/android_hardware_display_DisplayViewport.cpp
@@ -59,7 +59,8 @@
     static const jclass intClass = FindClassOrDie(env, "java/lang/Integer");
     static const jmethodID byteValue = env->GetMethodID(intClass, "byteValue", "()B");
 
-    viewport->displayId = env->GetIntField(viewportObj, gDisplayViewportClassInfo.displayId);
+    viewport->displayId = ui::LogicalDisplayId{
+            env->GetIntField(viewportObj, gDisplayViewportClassInfo.displayId)};
     viewport->isActive = env->GetBooleanField(viewportObj, gDisplayViewportClassInfo.isActive);
     jint orientation = env->GetIntField(viewportObj, gDisplayViewportClassInfo.orientation);
     viewport->orientation = static_cast<ui::Rotation>(orientation);
diff --git a/core/jni/android_hardware_input_InputWindowHandle.cpp b/core/jni/android_hardware_input_InputWindowHandle.cpp
index bed7768..69f6334 100644
--- a/core/jni/android_hardware_input_InputWindowHandle.cpp
+++ b/core/jni/android_hardware_input_InputWindowHandle.cpp
@@ -165,8 +165,8 @@
     mInfo.ownerUid = gui::Uid{
             static_cast<uid_t>(env->GetIntField(obj, gInputWindowHandleClassInfo.ownerUid))};
     mInfo.packageName = getStringField(env, obj, gInputWindowHandleClassInfo.packageName, "<null>");
-    mInfo.displayId = env->GetIntField(obj,
-            gInputWindowHandleClassInfo.displayId);
+    mInfo.displayId =
+            ui::LogicalDisplayId{env->GetIntField(obj, gInputWindowHandleClassInfo.displayId)};
 
     jobject inputApplicationHandleObj = env->GetObjectField(obj,
             gInputWindowHandleClassInfo.inputApplicationHandle);
diff --git a/core/jni/android_view_KeyEvent.cpp b/core/jni/android_view_KeyEvent.cpp
index ca8752f..06e0d2d 100644
--- a/core/jni/android_view_KeyEvent.cpp
+++ b/core/jni/android_view_KeyEvent.cpp
@@ -135,8 +135,8 @@
     jlong eventTime = env->GetLongField(eventObj, gKeyEventClassInfo.mEventTime);
 
     KeyEvent event;
-    event.initialize(id, deviceId, source, displayId, *hmac, action, flags, keyCode, scanCode,
-                     metaState, repeatCount, downTime, eventTime);
+    event.initialize(id, deviceId, source, ui::LogicalDisplayId{displayId}, *hmac, action, flags,
+                     keyCode, scanCode, metaState, repeatCount, downTime, eventTime);
     return event;
 }
 
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index 3e3af40..f914bee 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -22,7 +22,6 @@
 #include <android_runtime/AndroidRuntime.h>
 #include <android_runtime/Log.h>
 #include <attestation/HmacKeyManager.h>
-#include <gui/constants.h>
 #include <input/Input.h>
 #include <log/log.h>
 #include <nativehelper/JNIHelp.h>
@@ -367,8 +366,8 @@
     ui::Transform transform;
     transform.set(xOffset, yOffset);
     ui::Transform identityTransform;
-    event->initialize(InputEvent::nextId(), deviceId, source, displayId, INVALID_HMAC, action, 0,
-                      flags, edgeFlags, metaState, buttonState,
+    event->initialize(InputEvent::nextId(), deviceId, source, ui::LogicalDisplayId{displayId},
+                      INVALID_HMAC, action, 0, flags, edgeFlags, metaState, buttonState,
                       static_cast<MotionClassification>(classification), transform, xPrecision,
                       yPrecision, AMOTION_EVENT_INVALID_CURSOR_POSITION,
                       AMOTION_EVENT_INVALID_CURSOR_POSITION, identityTransform, downTimeNanos,
@@ -646,13 +645,13 @@
 
 static jint android_view_MotionEvent_nativeGetDisplayId(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
-    return event->getDisplayId();
+    return static_cast<jint>(event->getDisplayId().val());
 }
 
 static void android_view_MotionEvent_nativeSetDisplayId(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr,
                                                         jint displayId) {
     MotionEvent* event = reinterpret_cast<MotionEvent*>(nativePtr);
-    return event->setDisplayId(displayId);
+    event->setDisplayId(ui::LogicalDisplayId{displayId});
 }
 
 static jint android_view_MotionEvent_nativeGetAction(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) {
diff --git a/core/jni/android_window_WindowInfosListener.cpp b/core/jni/android_window_WindowInfosListener.cpp
index bc69d1e6..c39d5e2 100644
--- a/core/jni/android_window_WindowInfosListener.cpp
+++ b/core/jni/android_window_WindowInfosListener.cpp
@@ -60,7 +60,7 @@
     }
     ScopedLocalRef<jobject> matrixObj(env, AMatrix_newInstance(env, transformValues));
     return env->NewObject(gDisplayInfoClassInfo.clazz, gDisplayInfoClassInfo.ctor,
-                          displayInfo.displayId, displayInfo.logicalWidth,
+                          displayInfo.displayId.val(), displayInfo.logicalWidth,
                           displayInfo.logicalHeight, matrixObj.get());
 }
 
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 12d62cc..062fab3 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -116,7 +116,7 @@
 
 using android::zygote::ZygoteFailure;
 
-using Action = android_mallopt_gwp_asan_options_t::Action;
+using Mode = android_mallopt_gwp_asan_options_t::Mode;
 
 // This type is duplicated in fd_utils.h
 typedef const std::function<void(std::string)>& fail_fn_t;
@@ -2101,21 +2101,21 @@
     switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) {
         default:
         case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT:
-            gwp_asan_options.desire = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true)
-                    ? Action::TURN_ON_FOR_APP_SAMPLED_NON_CRASHING
-                    : Action::DONT_TURN_ON_UNLESS_OVERRIDDEN;
+            gwp_asan_options.mode = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true)
+                    ? Mode::APP_MANIFEST_DEFAULT
+                    : Mode::APP_MANIFEST_NEVER;
             android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
             break;
         case RuntimeFlags::GWP_ASAN_LEVEL_NEVER:
-            gwp_asan_options.desire = Action::DONT_TURN_ON_UNLESS_OVERRIDDEN;
+            gwp_asan_options.mode = Mode::APP_MANIFEST_NEVER;
             android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
             break;
         case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS:
-            gwp_asan_options.desire = Action::TURN_ON_FOR_APP;
+            gwp_asan_options.mode = Mode::APP_MANIFEST_ALWAYS;
             android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
             break;
         case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY:
-            gwp_asan_options.desire = Action::TURN_ON_WITH_SAMPLING;
+            gwp_asan_options.mode = Mode::APP_MANIFEST_DEFAULT;
             android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
             break;
     }
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 0433855..bf2fdda 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -329,7 +329,8 @@
 
         InputDeviceInfo info = InputDeviceInfo();
         info.initialize(keyboardId, 0, 0, InputDeviceIdentifier(),
-                        "keyboard " + std::to_string(keyboardId), true, false, 0);
+                        "keyboard " + std::to_string(keyboardId), true, false,
+                        ui::ADISPLAY_ID_DEFAULT);
         info.setKeyboardType(AINPUT_KEYBOARD_TYPE_ALPHABETIC);
         info.setKeyCharacterMap(*charMap);
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index bfbfb3a..70d923b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8771,6 +8771,7 @@
 
         <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService"
                  android:permission="android.permission.BIND_INCALL_SERVICE"
+                 android:enabled="@bool/config_enableContextSyncInCall"
                  android:exported="true">
             <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
                        android:value="true" />
diff --git a/core/res/res/drawable-car/car_activity_resolver_list_background.xml b/core/res/res/drawable-car/car_activity_resolver_list_background.xml
new file mode 100644
index 0000000..dbbadd8
--- /dev/null
+++ b/core/res/res/drawable-car/car_activity_resolver_list_background.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="?attr/colorBackgroundFloating" />
+    <corners android:radius="@dimen/car_activity_resolver_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
index c0fe536..7c45c20 100644
--- a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
@@ -22,7 +22,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 11 13 L 2 22 L 11 22 Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 1 bar. move to higher ground. -->
+        <path
+            android:name="ic_signal_cellular_1_4_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H11 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
index 816da22..02b646d 100644
--- a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
@@ -22,7 +22,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M8.72,15.28,2,22H8.72V15.28Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 1 bar. might have to call you back. -->
+        <path
+            android:name="ic_signal_cellular_1_5_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H12 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
index 69a966b..514d169 100644
--- a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
@@ -22,7 +22,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 13 11 L 2 22 L 13 22 Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 2 bars. 2 out of 4 ain't bad. -->
+        <path
+            android:name="ic_signal_cellular_2_4_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H14 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
index 02c7a43..a97f771 100644
--- a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
@@ -23,7 +23,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 11.45 12.55 L 2 22 L 11.45 22 L 11.45 12.55 Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 2 bars. hanging in there. -->
+        <path
+            android:name="ic_signal_cellular_2_5_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H14 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
index 46ce47c..1bacf4a 100644
--- a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
@@ -22,7 +22,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 2 22 L 16 22 L 16 21 L 16 20 L 16 11 L 16 10 L 16 8 Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 3 bars. quite nice. -->
+        <path
+            android:name="ic_signal_cellular_3_4_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H17 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
index 37435e6b..2789d3e 100644
--- a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
@@ -22,7 +22,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 14.96 9.04 L 2 22 L 14.96 22 L 14.96 9.04 Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 3 bars. not great, not terrible. -->
+        <path
+            android:name="ic_signal_cellular_3_5_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H16 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
index 6dc3646..8286dbb 100644
--- a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
@@ -22,7 +22,11 @@
     <path
         android:fillColor="@android:color/white"
         android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
-    <path
-        android:fillColor="@android:color/white"
-        android:pathData="M 18.48 5.52 L 2 22 L 18.48 22 L 18.48 5.52 Z" />
+    <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+        <!-- 4 bars. extremely respectable. -->
+        <path
+            android:name="ic_signal_cellular_4_5_bar"
+            android:fillColor="@android:color/white"
+            android:pathData="M6,0 H18 V20 H6 z" />
+    </clip-path>
 </vector>
\ No newline at end of file
diff --git a/core/res/res/layout-car/car_resolver_list.xml b/core/res/res/layout-car/car_resolver_list.xml
index 755cbfe..75266b2 100644
--- a/core/res/res/layout-car/car_resolver_list.xml
+++ b/core/res/res/layout-car/car_resolver_list.xml
@@ -18,147 +18,129 @@
 -->
 <com.android.internal.widget.ResolverDrawerLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
+    android:layout_width="@dimen/car_activity_resolver_width"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
     android:id="@id/contentPanel">
 
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:weightSum="5"
-        android:layout_alwaysShow="true"
-        android:orientation="vertical"
-        android:background="?attr/colorBackgroundFloating"
-        android:elevation="8dp">
+        android:layout_height="wrap_content"
+        android:weightSum="2"
+        android:orientation="vertical" >
+
+        <View android:id="@+id/empty_top"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@android:color/transparent" />
 
         <LinearLayout
-            android:id="@+id/button_bar"
-            android:visibility="gone"
-            style="?attr/buttonBarStyle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_ignoreOffset="true"
-            android:layout_alwaysShow="true"
-            android:layout_hasNestedScrollIndicator="true"
-            android:background="?attr/colorBackgroundFloating"
-            android:orientation="horizontal"
-            android:paddingTop="8dp"
-            android:paddingStart="12dp"
-            android:weightSum="4"
-            android:paddingEnd="12dp"
-            android:elevation="8dp">
+            android:orientation="vertical"
+            android:background="@drawable/car_activity_resolver_list_background">
 
-            <TextView
-                android:id="@+id/profile_button"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginEnd="8dp"
-                android:paddingStart="8dp"
-                android:paddingEnd="8dp"
-                android:textSize="40sp"
-                android:layout_weight="4"
-                android:layout_gravity="left"
-                android:visibility="gone"
-                android:textColor="?attr/colorAccent"
-                android:singleLine="true"/>
-
-            <TextView
-                android:id="@+id/title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="left"
-                android:layout_weight="3"
-                android:paddingTop="8dp"
-                android:layout_below="@id/profile_button"
-                android:textAppearance="?android:attr/textAppearanceLarge"
-                android:paddingBottom="8dp"/>
-
-            <Button
-                android:id="@+id/button_once"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:enabled="false"
-                android:layout_gravity="right"
-                style="?attr/buttonBarButtonStyle"
-                android:text="@string/activity_resolver_use_once"
-                android:layout_weight="0.5"
-                android:onClick="onButtonClick"/>
-
-            <Button
-                android:id="@+id/button_always"
-                android:layout_marginLeft="2dp"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:enabled="false"
-                android:layout_gravity="right"
-                style="?attr/buttonBarButtonStyle"
-                android:text="@string/activity_resolver_use_always"
-                android:layout_weight="0.5"
-                android:onClick="onButtonClick"/>
-        </LinearLayout>
-
-        <FrameLayout
-            android:id="@+id/stub"
-            android:visibility="gone"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="?attr/colorBackgroundFloating"/>
-
-        <TabHost
-            android:id="@+id/profile_tabhost"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
-            android:layout_centerHorizontal="true"
-            android:background="?attr/colorBackgroundFloating">
             <LinearLayout
-                android:orientation="vertical"
                 android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <TabWidget
-                    android:id="@android:id/tabs"
-                    android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@drawable/car_activity_resolver_list_background"
+                android:orientation="horizontal"
+                android:paddingVertical="@dimen/car_padding_4"
+                android:paddingHorizontal="@dimen/car_padding_4" >
+                <TextView
+                    android:id="@+id/profile_button"
+                    android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:visibility="gone">
-                </TabWidget>
-                <View
-                    android:id="@+id/resolver_tab_divider"
-                    android:visibility="gone"
-                    android:layout_width="match_parent"
-                    android:layout_height="1dp"
-                    android:background="?attr/colorBackgroundFloating"
-                    android:foreground="?attr/dividerVertical"
-                    android:layout_marginBottom="8dp"/>
-                <FrameLayout
-                    android:id="@android:id/tabcontent"
+                    android:visibility="gone" />
+
+                <TextView
+                    android:id="@+id/title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="start"
+                    android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
+            </LinearLayout>
+
+            <FrameLayout
+                android:id="@+id/stub"
+                android:visibility="gone"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"/>
+
+            <TabHost
+                android:id="@+id/profile_tabhost"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentTop="true"
+                android:layout_centerHorizontal="true"
+                android:background="?android:attr/colorBackgroundFloating">
+                <LinearLayout
+                    android:orientation="vertical"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content">
-                    <com.android.internal.app.ResolverViewPager
-                        android:id="@+id/profile_pager"
+                    <TabWidget
+                        android:id="@android:id/tabs"
+                        android:visibility="gone"
                         android:layout_width="match_parent"
-                        android:layout_height="wrap_content"/>
-                </FrameLayout>
+                        android:layout_height="wrap_content">
+                    </TabWidget>
+                    <View
+                        android:id="@+id/resolver_tab_divider"
+                        android:visibility="gone"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content" />
+                    <FrameLayout
+                        android:id="@android:id/tabcontent"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content">
+                        <com.android.internal.app.ResolverViewPager
+                            android:id="@+id/profile_pager"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"/>
+                    </FrameLayout>
+                </LinearLayout>
+            </TabHost>
+
+            <LinearLayout
+                android:id="@+id/button_bar"
+                android:visibility="gone"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginVertical="@dimen/car_padding_4"
+                android:layout_marginHorizontal="@dimen/car_padding_4"
+                android:padding="0dp"
+                android:gravity="center"
+                android:background="@drawable/car_activity_resolver_list_background"
+                android:orientation="vertical">
+
+                <Button
+                    android:id="@+id/button_once"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/car_button_height"
+                    android:enabled="false"
+                    android:layout_gravity="center"
+                    android:layout_marginBottom="@dimen/car_padding_2"
+                    android:text="@string/activity_resolver_use_once"
+                    android:onClick="onButtonClick"/>
+
+                <Button
+                    android:id="@+id/button_always"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/car_button_height"
+                    android:enabled="false"
+                    android:layout_gravity="center"
+                    android:text="@string/activity_resolver_use_always"
+                    android:onClick="onButtonClick"/>
             </LinearLayout>
-        </TabHost>
+        </LinearLayout>
 
-        <View
-            android:layout_alwaysShow="true"
+        <View android:id="@+id/empty_bottom"
             android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:background="?attr/colorBackgroundFloating"
-            android:foreground="?attr/dividerVertical"/>
-
-        <TextView android:id="@+id/empty"
-                  android:layout_width="match_parent"
-                  android:layout_height="wrap_content"
-                  android:background="?attr/colorBackgroundFloating"
-                  android:elevation="8dp"
-                  android:layout_alwaysShow="true"
-                  android:text="@string/noApplications"
-                  android:padding="32dp"
-                  android:gravity="center"
-                  android:visibility="gone"/>
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@android:color/transparent" />
 
     </LinearLayout>
 
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.internal.widget.ResolverDrawerLayout>
\ No newline at end of file
diff --git a/core/res/res/layout-car/car_resolver_list_with_default.xml b/core/res/res/layout-car/car_resolver_list_with_default.xml
index 5e450b2..0beb95b 100644
--- a/core/res/res/layout-car/car_resolver_list_with_default.xml
+++ b/core/res/res/layout-car/car_resolver_list_with_default.xml
@@ -18,179 +18,162 @@
 -->
 <com.android.internal.widget.ResolverDrawerLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:maxCollapsedHeight="200dp"
+    android:layout_width="@dimen/car_activity_resolver_width"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
     android:id="@id/contentPanel">
 
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:weightSum="5"
-        android:layout_alwaysShow="true"
-        android:orientation="vertical"
-        android:background="?attr/colorBackgroundFloating"
-        android:elevation="8dp">
+        android:layout_height="wrap_content"
+        android:weightSum="2"
+        android:orientation="vertical" >
+
+        <View android:id="@+id/empty_top"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@android:color/transparent" />
 
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_weight="0.5"
-            android:orientation="horizontal">
+            android:orientation="vertical"
+            android:layout_gravity="center"
+            android:background="@drawable/car_activity_resolver_list_background">
 
-            <ImageView
-                android:id="@+id/icon"
-                android:layout_width="60dp"
-                android:layout_height="60dp"
-                android:layout_gravity="start|top"
-                android:layout_marginStart="10dp"
-                android:layout_marginEnd="5dp"
-                android:layout_marginTop="10dp"
-                android:src="@drawable/resolver_icon_placeholder"
-                android:scaleType="fitCenter"/>
+            <FrameLayout
+                android:id="@+id/stub"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@drawable/car_activity_resolver_list_background"/>
 
-            <TextView
-                android:id="@+id/title"
-                android:layout_width="0dp"
-                android:layout_weight="1"
-                android:layout_height="?attr/listPreferredItemHeight"
-                android:layout_marginStart="16dp"
-                android:textAppearance="?android:attr/textAppearanceLarge"
-                android:gravity="start|center_vertical"
-                android:paddingEnd="16dp"/>
 
             <LinearLayout
-                android:id="@+id/profile_button"
-                android:layout_width="wrap_content"
-                android:layout_height="48dp"
-                android:layout_marginTop="4dp"
-                android:layout_marginEnd="4dp"
-                android:paddingStart="8dp"
-                android:paddingEnd="8dp"
-                android:paddingTop="4dp"
-                android:paddingBottom="4dp"
-                android:focusable="true"
-                android:visibility="gone"
-                style="?attr/borderlessButtonStyle">
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="@dimen/car_activity_resolver_list_item_height"
+                android:orientation="horizontal">
+
+                <RadioButton
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:focusable="false"
+                    android:clickable="false"
+                    android:layout_marginStart="?attr/listPreferredItemPaddingStart"
+                    android:layout_gravity="start|center_vertical"
+                    android:checked="true"/>
 
                 <ImageView
                     android:id="@+id/icon"
-                    android:layout_width="24dp"
-                    android:layout_height="24dp"
+                    android:layout_width="@dimen/car_icon_size"
+                    android:layout_height="@dimen/car_icon_size"
                     android:layout_gravity="start|center_vertical"
-                    android:layout_marginEnd="?attr/listPreferredItemPaddingEnd"
-                    android:layout_marginTop="12dp"
-                    android:layout_marginBottom="12dp"
+                    android:layout_marginStart="@dimen/car_padding_4"
+                    android:src="@drawable/resolver_icon_placeholder"
                     android:scaleType="fitCenter"/>
 
                 <TextView
-                    android:id="@id/text1"
+                    android:id="@+id/title"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_gravity="start|center_vertical"
-                    android:layout_marginEnd="?attr/listPreferredItemPaddingEnd"
-                    android:textAppearance="?attr/textAppearanceButton"
-                    android:textColor="?attr/textColorPrimary"
-                    android:minLines="1"
-                    android:maxLines="1"
-                    android:ellipsize="marquee"/>
-            </LinearLayout>
-        </LinearLayout>
+                    android:layout_marginHorizontal="?attr/listPreferredItemPaddingStart"
+                    style="?android:attr/textAppearanceListItem"
+                    android:layout_gravity="start|center_vertical" />
 
-        <LinearLayout
-            android:id="@+id/button_bar"
-            android:visibility="gone"
-            style="?attr/buttonBarStyle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alwaysShow="true"
-            android:gravity="end|center_vertical"
-            android:layout_weight="0.5"
-            android:orientation="horizontal"
-            android:layoutDirection="locale"
-            android:measureWithLargestChild="true"
-            android:paddingTop="8dp"
-            android:paddingBottom="8dp"
-            android:paddingStart="12dp"
-            android:paddingEnd="12dp"
-            android:elevation="8dp">
-
-            <Button
-                android:id="@+id/button_once"
-                android:layout_width="wrap_content"
-                android:layout_gravity="start"
-                android:maxLines="2"
-                style="?attr/buttonBarButtonStyle"
-                android:minHeight="@dimen/alert_dialog_button_bar_height"
-                android:layout_height="wrap_content"
-                android:enabled="false"
-                android:text="@string/activity_resolver_use_once"
-                android:onClick="onButtonClick"/>
-
-            <Button
-                android:id="@+id/button_always"
-                android:layout_width="wrap_content"
-                android:layout_gravity="end"
-                android:maxLines="2"
-                android:minHeight="@dimen/alert_dialog_button_bar_height"
-                style="?attr/buttonBarButtonStyle"
-                android:layout_height="wrap_content"
-                android:enabled="false"
-                android:text="@string/activity_resolver_use_always"
-                android:onClick="onButtonClick"/>
-        </LinearLayout>
-
-        <FrameLayout
-            android:id="@+id/stub"
-            android:layout_alwaysShow="true"
-            android:visibility="gone"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="?attr/colorBackgroundFloating"/>
-
-        <TabHost
-            android:layout_alwaysShow="true"
-            android:id="@+id/profile_tabhost"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
-            android:layout_centerHorizontal="true"
-            android:background="?attr/colorBackgroundFloating">
-            <LinearLayout
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <TabWidget
-                    android:id="@android:id/tabs"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:visibility="gone">
-                </TabWidget>
-                <View
-                    android:id="@+id/resolver_tab_divider"
+                <LinearLayout
+                    android:id="@+id/profile_button"
                     android:visibility="gone"
-                    android:layout_width="match_parent"
-                    android:layout_height="1dp"
-                    android:background="?attr/colorBackgroundFloating"
-                    android:foreground="?attr/dividerVertical"
-                    android:layout_marginBottom="8dp"/>
-                <FrameLayout
-                    android:id="@android:id/tabcontent"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content">
+
+                    <ImageView
+                        android:id="@+id/icon"
+                        android:visibility="gone"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content" />
+
+                    <TextView
+                        android:id="@id/text1"
+                        android:visibility="gone"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content" />
+                </LinearLayout>
+            </LinearLayout>
+
+            <TabHost
+                android:id="@+id/profile_tabhost"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_alignParentTop="true"
+                android:layout_centerHorizontal="true"
+                android:background="?attr/colorBackgroundFloating">
+                <LinearLayout
+                    android:orientation="vertical"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content">
-                    <com.android.internal.app.ResolverViewPager
-                        android:id="@+id/profile_pager"
+                    <TabWidget
+                        android:id="@android:id/tabs"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:visibility="gone">
+                    </TabWidget>
+                    <View
+                        android:id="@+id/resolver_tab_divider"
+                        android:visibility="gone"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content" />
+                    <FrameLayout
+                        android:id="@android:id/tabcontent"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content">
-                    </com.android.internal.app.ResolverViewPager>
-                </FrameLayout>
-            </LinearLayout>
-        </TabHost>
+                        <com.android.internal.app.ResolverViewPager
+                            android:id="@+id/profile_pager"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content">
+                        </com.android.internal.app.ResolverViewPager>
+                    </FrameLayout>
+                </LinearLayout>
+            </TabHost>
 
-        <View
+            <LinearLayout
+                android:id="@+id/button_bar"
+                android:visibility="gone"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginVertical="@dimen/car_padding_4"
+                android:layout_marginHorizontal="@dimen/car_padding_4"
+                android:gravity="center"
+                android:background="@drawable/car_activity_resolver_list_background"
+                android:orientation="vertical">
+
+                <Button
+                    android:id="@+id/button_once"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/car_button_height"
+                    android:enabled="false"
+                    android:layout_gravity="center"
+                    android:layout_marginBottom="@dimen/car_padding_2"
+                    android:text="@string/activity_resolver_use_once"
+                    android:onClick="onButtonClick"/>
+
+                <Button
+                    android:id="@+id/button_always"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/car_button_height"
+                    android:enabled="false"
+                    android:layout_gravity="center"
+                    android:text="@string/activity_resolver_use_always"
+                    android:onClick="onButtonClick"/>
+            </LinearLayout>
+        </LinearLayout>
+
+        <View android:id="@+id/empty_bottom"
             android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:background="?attr/dividerVertical"/>
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@android:color/transparent" />
+
     </LinearLayout>
 
 </com.android.internal.widget.ResolverDrawerLayout>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 877d11e..cefc648 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6093,6 +6093,18 @@
     <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
     <bool name="config_letterboxIsEducationEnabled">false</bool>
 
+    <!-- The width in dp to use to detect vertical thin letterboxing.
+         If W is the available width and w is the letterbox width, an app
+         is thin letterboxed if the value here is < (W - w) / 2
+         If the value is < 0 the thin letterboxing policy is disabled -->
+    <dimen name="config_letterboxThinLetterboxWidthDp">-1dp</dimen>
+
+    <!-- The height in dp to use to detect horizontal thin letterboxing
+         If H is the available height and h is the letterbox height, an app
+         is thin letterboxed if the value here is < (H - h) / 2
+         If the value is < 0 the thin letterboxing policy is disabled -->
+    <dimen name="config_letterboxThinLetterboxHeightDp">-1dp</dimen>
+
     <!-- Default min aspect ratio for unresizable apps which are eligible for size compat mode.
          Values <= 1.0 will be ignored. Activity min/max aspect ratio restrictions will still be
          espected so this override can control the maximum screen area that can be occupied by
@@ -7039,6 +7051,9 @@
          event gets ignored. -->
     <integer name="config_defaultMinEmergencyGestureTapDurationMillis">200</integer>
 
+    <!-- Control whether to enable CallMetadataSyncInCallService. -->
+    <bool name="config_enableContextSyncInCall">false</bool>
+
     <!-- Whether the system uses auto-suspend mode. -->
     <bool name="config_useAutoSuspend">true</bool>
 </resources>
diff --git a/core/res/res/values/dimens_car.xml b/core/res/res/values/dimens_car.xml
index c5dddb8..99377ff 100644
--- a/core/res/res/values/dimens_car.xml
+++ b/core/res/res/values/dimens_car.xml
@@ -151,4 +151,10 @@
     <dimen name="action_bar_button_margin">@*android:dimen/car_padding_4</dimen>
     <dimen name="action_bar_button_max_width">268dp</dimen>
     <dimen name="action_bar_toggle_internal_padding">@*android:dimen/car_padding_3</dimen>
+
+    <!-- Intent Resolver -->
+    <dimen name="car_activity_resolver_width">706dp</dimen>
+    <dimen name="car_activity_resolver_list_item_height">96dp</dimen>
+    <dimen name="car_activity_resolver_list_max_height">256dp</dimen>
+    <dimen name="car_activity_resolver_corner_radius">24dp</dimen>
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 1fca4f8..59e4161 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6484,4 +6484,23 @@
     <string name="satellite_notification_how_it_works">How it works</string>
     <!-- Initial/System provided label shown for an app which gets unarchived. [CHAR LIMIT=64]. -->
     <string name="unarchival_session_app_label">Pending...</string>
+
+    <!-- Fingerprint dangling notification title -->
+    <string name="fingerprint_dangling_notification_title">Set up Fingerprint Unlock again</string>
+    <!-- Fingerprint dangling notification content for only 1 fingerprint deleted -->
+    <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted to improve performance</string>
+    <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted -->
+    <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted to improve performance</string>
+    <!-- Fingerprint dangling notification content for only 1 fingerprint deleted and no fingerprint left-->
+    <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted. Set it up again to unlock your phone with fingerprint.</string>
+    <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted and no fingerprint left  -->
+    <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted. Set them up again to unlock your phone with your fingerprint.</string>
+    <!-- Face dangling notification title -->
+    <string name="face_dangling_notification_title">Set up Face Unlock again</string>
+    <!-- Face dangling notification content -->
+    <string name="face_dangling_notification_msg">Your face model wasn\'t working well and was deleted. Set it up again to unlock your phone with face.</string>
+    <!-- Biometric dangling notification "set up" action button -->
+    <string name="biometric_dangling_notification_action_set_up">Set up</string>
+    <!-- Biometric dangling notification "Not now" action button -->
+    <string name="biometric_dangling_notification_action_not_now">Not now</string>
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 54dbc48..d058fb1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4717,6 +4717,8 @@
   <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" />
   <java-symbol type="bool" name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled" />
   <java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
+  <java-symbol type="dimen" name="config_letterboxThinLetterboxWidthDp" />
+  <java-symbol type="dimen" name="config_letterboxThinLetterboxHeightDp" />
   <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
   <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
   <java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" />
@@ -5430,4 +5432,15 @@
 
   <!-- For PowerManagerService to determine whether to use auto-suspend mode -->
   <java-symbol type="bool" name="config_useAutoSuspend" />
+
+  <!-- Biometric dangling notification strings -->
+  <java-symbol type="string" name="fingerprint_dangling_notification_title" />
+  <java-symbol type="string" name="fingerprint_dangling_notification_msg_1" />
+  <java-symbol type="string" name="fingerprint_dangling_notification_msg_2" />
+  <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_1" />
+  <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_2" />
+  <java-symbol type="string" name="face_dangling_notification_title" />
+  <java-symbol type="string" name="face_dangling_notification_msg" />
+  <java-symbol type="string" name="biometric_dangling_notification_action_set_up" />
+  <java-symbol type="string" name="biometric_dangling_notification_action_not_now" />
 </resources>
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 0b1b40c..bc0ae9f 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -623,6 +623,28 @@
         assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory());
     }
 
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY
+    })
+    public void idleDetected() throws Throwable {
+        waitForFrameRateCategoryToSettle();
+        mActivityRule.runOnUiThread(() -> {
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
+            mMovingView.setFrameContentVelocity(Float.MAX_VALUE);
+            mMovingView.invalidate();
+            runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
+                    mViewRoot.getLastPreferredFrameRateCategory()));
+        });
+        waitForAfterDraw();
+
+        // Wait for idle timeout
+        Thread.sleep(500);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
     private void runAfterDraw(@NonNull Runnable runnable) {
         Handler handler = new Handler(Looper.getMainLooper());
         mAfterDrawLatch = new CountDownLatch(1);
diff --git a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
new file mode 100644
index 0000000..bcdac61
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util;
+
+import static junit.framework.Assert.assertEquals;
+
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link NewlineNormalizer}
+ * @hide
+ */
+@DisabledOnRavenwood(blockedBy = NewlineNormalizer.class)
+@RunWith(AndroidJUnit4.class)
+public class NewlineNormalizerTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Test
+    public void testEmptyInput() {
+        assertEquals("", NewlineNormalizer.normalizeNewlines(""));
+    }
+
+    @Test
+    public void testSingleNewline() {
+        assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n"));
+    }
+
+    @Test
+    public void testMultipleConsecutiveNewlines() {
+        assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n\n\n\n\n"));
+    }
+
+    @Test
+    public void testNewlinesWithSpacesAndTabs() {
+        String input = "Line 1\n  \n \t \n\tLine 2";
+        // Adjusted expected output to include the tab character
+        String expected = "Line 1\n\tLine 2";
+        assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
+    }
+
+    @Test
+    public void testMixedNewlineCharacters() {
+        String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6";
+        String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6";
+        assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
+    }
+}
diff --git a/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java
index 635e78e..cc5b3b9 100644
--- a/graphics/java/android/graphics/RecordingCanvas.java
+++ b/graphics/java/android/graphics/RecordingCanvas.java
@@ -40,7 +40,7 @@
 
     /** @hide */
     private static int getPanelFrameSize() {
-        final int DefaultSize = 100 * 1024 * 1024; // 100 MB;
+        final int DefaultSize = 150 * 1024 * 1024; // 150 MB;
         return Math.max(SystemProperties.getInt("ro.hwui.max_texture_allocation_size", DefaultSize),
                 DefaultSize);
     }
@@ -262,7 +262,7 @@
     protected void throwIfCannotDraw(Bitmap bitmap) {
         super.throwIfCannotDraw(bitmap);
         int bitmapSize = bitmap.getByteCount();
-        if (bitmapSize > MAX_BITMAP_SIZE) {
+        if (bitmap.getConfig() != Bitmap.Config.HARDWARE && bitmapSize > MAX_BITMAP_SIZE) {
             throw new RuntimeException(
                     "Canvas: trying to draw too large(" + bitmapSize + "bytes) bitmap.");
         }
diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java
deleted file mode 100644
index d1d7c14..0000000
--- a/keystore/java/android/security/KeyStore.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2009 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.security;
-
-/**
- * This class provides some constants and helper methods related to Android's Keystore service.
- * This class was originally much larger, but its functionality was superseded by other classes.
- * It now just contains a few remaining pieces for which the users haven't been updated yet.
- * You may be looking for {@link java.security.KeyStore} instead.
- *
- * @hide
- */
-public class KeyStore {
-
-    // Used for UID field to indicate the calling UID.
-    public static final int UID_SELF = -1;
-}
diff --git a/ktfmt_includes.txt b/ktfmt_includes.txt
index fe47503..0ac6265 100644
--- a/ktfmt_includes.txt
+++ b/ktfmt_includes.txt
@@ -5,8 +5,6 @@
 -packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
 -packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
 -packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
 -packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
 -packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt
 -packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
index d923a46..d241641 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
@@ -16,6 +16,8 @@
 
 package androidx.window.common;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.Application;
 import android.os.Bundle;
@@ -26,30 +28,30 @@
  */
 public class EmptyLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks {
     @Override
-    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
     }
 
     @Override
-    public void onActivityStarted(Activity activity) {
+    public void onActivityStarted(@NonNull Activity activity) {
     }
 
     @Override
-    public void onActivityResumed(Activity activity) {
+    public void onActivityResumed(@NonNull Activity activity) {
     }
 
     @Override
-    public void onActivityPaused(Activity activity) {
+    public void onActivityPaused(@NonNull Activity activity) {
     }
 
     @Override
-    public void onActivityStopped(Activity activity) {
+    public void onActivityStopped(@NonNull Activity activity) {
     }
 
     @Override
-    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
     }
 
     @Override
-    public void onActivityDestroyed(Activity activity) {
+    public void onActivityDestroyed(@NonNull Activity activity) {
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index a0d6fce..6793fa5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -35,6 +35,7 @@
 
 import android.annotation.DimenRes;
 import android.annotation.Nullable;
+import android.app.Activity;
 import android.app.ActivityThread;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
@@ -42,6 +43,7 @@
 import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.RotateDrawable;
 import android.hardware.display.DisplayManager;
@@ -213,7 +215,11 @@
                             isVerticalSplit,
                             isReversedLayout,
                             parentInfo.getDisplayId(),
-                            isDraggableExpandType
+                            isDraggableExpandType,
+                            getContainerBackgroundColor(topSplitContainer.getPrimaryContainer(),
+                                    DEFAULT_PRIMARY_VEIL_COLOR),
+                            getContainerBackgroundColor(topSplitContainer.getSecondaryContainer(),
+                                    DEFAULT_SECONDARY_VEIL_COLOR)
                     ));
         }
     }
@@ -242,6 +248,27 @@
     }
 
     /**
+     * Returns the window background color of the top activity in the container if set, or the
+     * default color if the background color of the top activity is unavailable.
+     */
+    @VisibleForTesting
+    @NonNull
+    static Color getContainerBackgroundColor(
+            @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) {
+        final Activity activity = container.getTopNonFinishingActivity();
+        if (activity == null || !activity.isResumed()) {
+            // This can happen when the top activity in the container is from a different process.
+            return defaultColor;
+        }
+
+        final Drawable drawable = activity.getWindow().getDecorView().getBackground();
+        if (drawable instanceof ColorDrawable colorDrawable) {
+            return Color.valueOf(colorDrawable.getColor());
+        }
+        return defaultColor;
+    }
+
+    /**
      * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner
      * of the existing decor surface to be the specified TaskFragment.
      *
@@ -800,6 +827,8 @@
         private final int mDisplayId;
         private final boolean mIsReversedLayout;
         private final boolean mIsDraggableExpandType;
+        private final Color mPrimaryVeilColor;
+        private final Color mSecondaryVeilColor;
 
         @VisibleForTesting
         Properties(
@@ -810,7 +839,9 @@
                 boolean isVerticalSplit,
                 boolean isReversedLayout,
                 int displayId,
-                boolean isDraggableExpandType) {
+                boolean isDraggableExpandType,
+                @NonNull Color primaryVeilColor,
+                @NonNull Color secondaryVeilColor) {
             mConfiguration = configuration;
             mDividerAttributes = dividerAttributes;
             mDecorSurface = decorSurface;
@@ -819,6 +850,8 @@
             mIsReversedLayout = isReversedLayout;
             mDisplayId = displayId;
             mIsDraggableExpandType = isDraggableExpandType;
+            mPrimaryVeilColor = primaryVeilColor;
+            mSecondaryVeilColor = secondaryVeilColor;
         }
 
         /**
@@ -840,7 +873,9 @@
                     && a.mIsVerticalSplit == b.mIsVerticalSplit
                     && a.mDisplayId == b.mDisplayId
                     && a.mIsReversedLayout == b.mIsReversedLayout
-                    && a.mIsDraggableExpandType == b.mIsDraggableExpandType;
+                    && a.mIsDraggableExpandType == b.mIsDraggableExpandType
+                    && a.mPrimaryVeilColor.equals(b.mPrimaryVeilColor)
+                    && a.mSecondaryVeilColor.equals(b.mSecondaryVeilColor);
         }
 
         private static boolean areSameSurfaces(
@@ -1087,8 +1122,8 @@
         }
 
         private void showVeils(@NonNull SurfaceControl.Transaction t) {
-            t.setColor(mPrimaryVeil, colorToFloatArray(DEFAULT_PRIMARY_VEIL_COLOR))
-                    .setColor(mSecondaryVeil, colorToFloatArray(DEFAULT_SECONDARY_VEIL_COLOR))
+            t.setColor(mPrimaryVeil, colorToFloatArray(mProperties.mPrimaryVeilColor))
+                    .setColor(mSecondaryVeil, colorToFloatArray(mProperties.mSecondaryVeilColor))
                     .setLayer(mDividerSurface, DIVIDER_LAYER)
                     .setLayer(mPrimaryVeil, VEIL_LAYER)
                     .setLayer(mSecondaryVeil, VEIL_LAYER)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index 56c3bce..339908a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -16,16 +16,10 @@
 
 package androidx.window.sidecar;
 
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
-
+import android.annotation.Nullable;
 import android.app.Activity;
-import android.app.ActivityThread;
 import android.app.Application;
 import android.content.Context;
-import android.graphics.Rect;
 import android.hardware.devicestate.DeviceStateManager;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -38,7 +32,6 @@
 import androidx.window.util.BaseDataProducer;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -76,64 +69,13 @@
     @NonNull
     @Override
     public SidecarDeviceState getDeviceState() {
-        SidecarDeviceState deviceState = new SidecarDeviceState();
-        deviceState.posture = deviceStateFromFeature();
-        return deviceState;
-    }
-
-    private int deviceStateFromFeature() {
-        for (int i = 0; i < mStoredFeatures.size(); i++) {
-            CommonFoldingFeature feature = mStoredFeatures.get(i);
-            final int state = feature.getState();
-            switch (state) {
-                case CommonFoldingFeature.COMMON_STATE_FLAT:
-                    return SidecarDeviceState.POSTURE_OPENED;
-                case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
-                    return SidecarDeviceState.POSTURE_HALF_OPENED;
-                case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
-                    return SidecarDeviceState.POSTURE_UNKNOWN;
-            }
-        }
-        return SidecarDeviceState.POSTURE_UNKNOWN;
+        return SidecarHelper.calculateDeviceState(mStoredFeatures);
     }
 
     @NonNull
     @Override
     public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
-        Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
-        SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
-        if (activity == null) {
-            return windowLayoutInfo;
-        }
-        windowLayoutInfo.displayFeatures = getDisplayFeatures(activity);
-        return windowLayoutInfo;
-    }
-
-    private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
-        int displayId = activity.getDisplay().getDisplayId();
-        if (displayId != DEFAULT_DISPLAY) {
-            return Collections.emptyList();
-        }
-
-        if (activity.isInMultiWindowMode()) {
-            // It is recommended not to report any display features in multi-window mode, since it
-            // won't be possible to synchronize the display feature positions with window movement.
-            return Collections.emptyList();
-        }
-
-        List<SidecarDisplayFeature> features = new ArrayList<>();
-        final int rotation = activity.getResources().getConfiguration().windowConfiguration
-                .getDisplayRotation();
-        for (CommonFoldingFeature baseFeature : mStoredFeatures) {
-            SidecarDisplayFeature feature = new SidecarDisplayFeature();
-            Rect featureRect = baseFeature.getRect();
-            rotateRectToDisplayRotation(displayId, rotation, featureRect);
-            transformToWindowSpaceRect(activity, featureRect);
-            feature.setRect(featureRect);
-            feature.setType(baseFeature.getType());
-            features.add(feature);
-        }
-        return Collections.unmodifiableList(features);
+        return SidecarHelper.calculateWindowLayoutInfo(windowToken, mStoredFeatures);
     }
 
     @Override
@@ -145,13 +87,14 @@
 
     private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter {
         @Override
-        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+        public void onActivityCreated(@NonNull Activity activity,
+                @Nullable Bundle savedInstanceState) {
             super.onActivityCreated(activity, savedInstanceState);
             onDisplayFeaturesChangedForActivity(activity);
         }
 
         @Override
-        public void onActivityConfigurationChanged(Activity activity) {
+        public void onActivityConfigurationChanged(@NonNull Activity activity) {
             super.onActivityConfigurationChanged(activity);
             onDisplayFeaturesChangedForActivity(activity);
         }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
new file mode 100644
index 0000000..bb6ab47
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -0,0 +1,129 @@
+/*
+ * 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 androidx.window.sidecar;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Rect;
+import android.os.IBinder;
+
+import androidx.window.common.CommonFoldingFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A utility class for transforming between Sidecar and Extensions features.
+ */
+class SidecarHelper {
+
+    private SidecarHelper() {}
+
+    /**
+     * Returns the {@link SidecarDeviceState} posture that is calculated for the first fold in
+     * the feature list. Sidecar devices only have one fold so we only pick the first one to
+     * determine the state.
+     * @param featureList the {@link CommonFoldingFeature} that are currently active.
+     * @return the {@link SidecarDeviceState} calculated from the {@link List} of
+     * {@link CommonFoldingFeature}.
+     */
+    @SuppressWarnings("deprecation")
+    private static int deviceStateFromFeatureList(@NonNull List<CommonFoldingFeature> featureList) {
+        for (int i = 0; i < featureList.size(); i++) {
+            final CommonFoldingFeature feature = featureList.get(i);
+            final int state = feature.getState();
+            switch (state) {
+                case CommonFoldingFeature.COMMON_STATE_FLAT:
+                    return SidecarDeviceState.POSTURE_OPENED;
+                case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
+                    return SidecarDeviceState.POSTURE_HALF_OPENED;
+                case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
+                    return SidecarDeviceState.POSTURE_UNKNOWN;
+                case CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES:
+                    return SidecarDeviceState.POSTURE_UNKNOWN;
+                case CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE:
+                    return SidecarDeviceState.POSTURE_UNKNOWN;
+            }
+        }
+        return SidecarDeviceState.POSTURE_UNKNOWN;
+    }
+
+    /**
+     * Returns a {@link SidecarDeviceState} calculated from a {@link List} of
+     * {@link CommonFoldingFeature}s.
+     */
+    @SuppressWarnings("deprecation")
+    static SidecarDeviceState calculateDeviceState(
+            @NonNull List<CommonFoldingFeature> featureList) {
+        final SidecarDeviceState deviceState = new SidecarDeviceState();
+        deviceState.posture = deviceStateFromFeatureList(featureList);
+        return deviceState;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static List<SidecarDisplayFeature> calculateDisplayFeatures(
+            @NonNull Activity activity,
+            @NonNull List<CommonFoldingFeature> featureList
+    ) {
+        final int displayId = activity.getDisplay().getDisplayId();
+        if (displayId != DEFAULT_DISPLAY) {
+            return Collections.emptyList();
+        }
+
+        if (activity.isInMultiWindowMode()) {
+            // It is recommended not to report any display features in multi-window mode, since it
+            // won't be possible to synchronize the display feature positions with window movement.
+            return Collections.emptyList();
+        }
+
+        final List<SidecarDisplayFeature> features = new ArrayList<>();
+        final int rotation = activity.getResources().getConfiguration().windowConfiguration
+                .getDisplayRotation();
+        for (CommonFoldingFeature baseFeature : featureList) {
+            final SidecarDisplayFeature feature = new SidecarDisplayFeature();
+            final Rect featureRect = baseFeature.getRect();
+            rotateRectToDisplayRotation(displayId, rotation, featureRect);
+            transformToWindowSpaceRect(activity, featureRect);
+            feature.setRect(featureRect);
+            feature.setType(baseFeature.getType());
+            features.add(feature);
+        }
+        return Collections.unmodifiableList(features);
+    }
+
+    /**
+     * Returns a {@link SidecarWindowLayoutInfo} calculated from the {@link List} of
+     * {@link CommonFoldingFeature}.
+     */
+    @SuppressWarnings("deprecation")
+    static SidecarWindowLayoutInfo calculateWindowLayoutInfo(@NonNull IBinder windowToken,
+            @NonNull List<CommonFoldingFeature> featureList) {
+        final Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
+        final SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
+        if (activity == null) {
+            return windowLayoutInfo;
+        }
+        windowLayoutInfo.displayFeatures = calculateDisplayFeatures(activity, featureList);
+        return windowLayoutInfo;
+    }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 8aca92e..ad913c9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -35,8 +35,11 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.Activity;
 import android.content.res.Configuration;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.os.Binder;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
@@ -44,6 +47,8 @@
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
+import android.view.View;
+import android.view.Window;
 import android.window.TaskFragmentOperation;
 import android.window.TaskFragmentParentInfo;
 import android.window.WindowContainerTransaction;
@@ -152,7 +157,10 @@
                 true /* isVerticalSplit */,
                 false /* isReversedLayout */,
                 Display.DEFAULT_DISPLAY,
-                false /* isDraggableExpandType */);
+                false /* isDraggableExpandType */,
+                Color.valueOf(Color.BLACK), /* primaryVeilColor */
+                Color.valueOf(Color.GRAY) /* secondaryVeilColor */
+        );
 
         mDividerPresenter = new DividerPresenter(
                 MOCK_TASK_ID, mDragEventCallback, mock(Executor.class));
@@ -604,6 +612,38 @@
                 0.0001 /* delta */);
     }
 
+    @Test
+    public void testGetContainerBackgroundColor() {
+        final Color defaultColor = Color.valueOf(Color.RED);
+        final Color activityBackgroundColor = Color.valueOf(Color.BLUE);
+        final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
+        final Activity activity = mock(Activity.class);
+        final Window window = mock(Window.class);
+        final View decorView = mock(View.class);
+        final ColorDrawable backgroundDrawable =
+                new ColorDrawable(activityBackgroundColor.toArgb());
+        when(activity.getWindow()).thenReturn(window);
+        when(window.getDecorView()).thenReturn(decorView);
+        when(decorView.getBackground()).thenReturn(backgroundDrawable);
+
+        // When the top non-finishing activity returns null, the default color should be returned.
+        when(container.getTopNonFinishingActivity()).thenReturn(null);
+        assertEquals(defaultColor,
+                DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+
+        // When the top non-finishing activity is not resumed, the default color should be returned.
+        when(container.getTopNonFinishingActivity()).thenReturn(activity);
+        when(activity.isResumed()).thenReturn(false);
+        assertEquals(defaultColor,
+                DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+
+        // When the top non-finishing activity is resumed, its background color should be returned.
+        when(container.getTopNonFinishingActivity()).thenReturn(activity);
+        when(activity.isResumed()).thenReturn(true);
+        assertEquals(activityBackgroundColor,
+                DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+    }
+
     private TaskFragmentContainer createMockTaskFragmentContainer(
             @NonNull IBinder token, @NonNull Rect bounds) {
         final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
index 6110133..0764141 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
@@ -31,7 +31,6 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.bubbles.BubblePositioner
 import com.android.wm.shell.bubbles.DeviceConfig
-import com.android.wm.shell.bubbles.bar.BubbleExpandedViewPinController.Companion.DROP_TARGET_SCALE
 import com.android.wm.shell.common.bubbles.BaseBubblePinController
 import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION
 import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
@@ -248,15 +247,8 @@
     private val dropTargetView: View?
         get() = container.findViewById(R.id.bubble_bar_drop_target)
 
-    private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect {
-        val rect = Rect()
-        positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect)
-        // Scale the rect to expected size, but keep the center point the same
-        val centerX = rect.centerX()
-        val centerY = rect.centerY()
-        rect.scale(DROP_TARGET_SCALE)
-        rect.offset(centerX - rect.centerX(), centerY - rect.centerY())
-        return rect
+    private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect = Rect().also {
+        positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it)
     }
 
     private fun runOnMainSync(runnable: Runnable) {
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
index 9dcde3b..b928a0b 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
@@ -13,12 +13,14 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-    <corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" />
-    <solid android:color="@color/bubble_drop_target_background_color" />
-    <stroke
-        android:width="1dp"
-        android:color="?androidprv:attr/materialColorPrimaryContainer" />
-</shape>
+    android:inset="@dimen/bubble_bar_expanded_view_drop_target_padding">
+    <shape android:shape="rectangle">
+        <corners android:radius="@dimen/bubble_bar_expanded_view_drop_target_corner" />
+        <solid android:color="@color/bubble_drop_target_background_color" />
+        <stroke
+            android:width="1dp"
+            android:color="?androidprv:attr/materialColorPrimaryContainer" />
+    </shape>
+</inset>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f532f96..8d24c16 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -274,6 +274,9 @@
     <dimen name="bubble_bar_expanded_view_corner_radius">16dp</dimen>
     <!-- Corner radius for expanded view while it is being dragged -->
     <dimen name="bubble_bar_expanded_view_corner_radius_dragged">28dp</dimen>
+    <!-- Corner radius for expanded view drop target -->
+    <dimen name="bubble_bar_expanded_view_drop_target_corner">28dp</dimen>
+    <dimen name="bubble_bar_expanded_view_drop_target_padding">24dp</dimen>
     <!-- Width of the box around bottom center of the screen where drag only leads to dismiss -->
     <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen>
     <!-- Height of the box around bottom center of the screen where drag only leads to dismiss -->
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 87aac0b..9e6c5fb 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
@@ -170,6 +170,8 @@
          * the pointer might need to be updated.
          */
         void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer);
+        /** Called when the bubble overflow empty state changes, used to show/hide the overflow. */
+        void bubbleOverflowChanged(boolean hasBubbles);
     }
 
     private final Context mContext;
@@ -1169,6 +1171,15 @@
      */
     public void startBubbleDrag(String bubbleKey) {
         onBubbleDrag(bubbleKey, true /* isBeingDragged */);
+        if (mBubbleStateListener != null) {
+            boolean overflow = BubbleOverflow.KEY.equals(bubbleKey);
+            Rect rect = new Rect();
+            mBubblePositioner.getBubbleBarExpandedViewBounds(mBubblePositioner.isBubbleBarOnLeft(),
+                    overflow, rect);
+            BubbleBarUpdate update = new BubbleBarUpdate();
+            update.expandedViewDropTargetSize = new Point(rect.width(), rect.height());
+            mBubbleStateListener.onBubbleStateChange(update);
+        }
     }
 
     /**
@@ -1401,7 +1412,7 @@
             Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
             if (b != null) {
                 // It's in the overflow, so remove it & reinflate
-                mBubbleData.removeOverflowBubble(b);
+                mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
             } else {
                 // App bubble does not exist, lets add and expand it
                 b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
@@ -1844,6 +1855,11 @@
             }
 
         }
+
+        @Override
+        public void bubbleOverflowChanged(boolean hasBubbles) {
+            // TODO (b/334175587): tell stack view to hide / show the overflow
+        }
     };
 
     /** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
@@ -1876,6 +1892,11 @@
         }
 
         @Override
+        public void bubbleOverflowChanged(boolean hasBubbles) {
+            // Nothing to do for our views, handled by launcher / in the bubble bar.
+        }
+
+        @Override
         public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
             if (mLayerView != null) {
                 // TODO (b/273316505) handle suppression changes, although might not need to
@@ -1914,7 +1935,7 @@
             ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
                     + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
                     + " expanded=%b selectionChanged=%b selected=%s"
-                    + " suppressed=%s unsupressed=%s shouldShowEducation=%b",
+                    + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b",
                     update.addedBubble != null ? update.addedBubble.getKey() : "null",
                     !update.removedBubbles.isEmpty(),
                     update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
@@ -1923,13 +1944,17 @@
                     update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
                     update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
                     update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
-                    update.shouldShowEducation);
+                    update.shouldShowEducation, update.showOverflowChanged);
 
             ensureBubbleViewsAndWindowCreated();
 
             // Lazy load overflow bubbles from disk
             loadOverflowBubblesFromDisk();
 
+            if (update.showOverflowChanged) {
+                mBubbleViewCallback.bubbleOverflowChanged(!update.overflowBubbles.isEmpty());
+            }
+
             // If bubbles in the overflow have a dot, make sure the overflow shows a dot
             updateOverflowButtonDot();
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index ae3d0c5..ea30af5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -77,6 +77,7 @@
         boolean suppressedSummaryChanged;
         boolean expanded;
         boolean shouldShowEducation;
+        boolean showOverflowChanged;
         @Nullable BubbleViewProvider selectedBubble;
         @Nullable Bubble addedBubble;
         @Nullable Bubble updatedBubble;
@@ -109,7 +110,8 @@
                     || suppressedBubble != null
                     || unsuppressedBubble != null
                     || suppressedSummaryChanged
-                    || suppressedSummaryGroup != null;
+                    || suppressedSummaryGroup != null
+                    || showOverflowChanged;
         }
 
         void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
@@ -157,6 +159,8 @@
                     bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey());
                 }
             }
+            bubbleBarUpdate.showOverflowChanged = showOverflowChanged;
+            bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty();
             return bubbleBarUpdate;
         }
 
@@ -410,6 +414,9 @@
             if (bubbleToReturn != null) {
                 // Promoting from overflow
                 mOverflowBubbles.remove(bubbleToReturn);
+                if (mOverflowBubbles.isEmpty()) {
+                    mStateChange.showOverflowChanged = true;
+                }
             } else if (mPendingBubbles.containsKey(key)) {
                 // Update while it was pending
                 bubbleToReturn = mPendingBubbles.get(key);
@@ -497,19 +504,6 @@
     }
 
     /**
-     * Explicitly removes a bubble from the overflow, if it exists.
-     *
-     * @param bubble the bubble to remove.
-     */
-    public void removeOverflowBubble(Bubble bubble) {
-        if (bubble == null) return;
-        if (mOverflowBubbles.remove(bubble)) {
-            mStateChange.removedOverflowBubble = bubble;
-            dispatchPendingChanges();
-        }
-    }
-
-    /**
      * Adds a group key indicating that the summary for this group should be suppressed.
      *
      * @param groupKey the group key of the group whose summary should be suppressed.
@@ -683,7 +677,6 @@
         if (indexToRemove == -1) {
             if (hasOverflowBubbleWithKey(key)
                     && shouldRemoveHiddenBubble) {
-
                 Bubble b = getOverflowBubbleWithKey(key);
                 ProtoLog.d(WM_SHELL_BUBBLES, "doRemove - cancel overflow bubble=%s", key);
                 if (b != null) {
@@ -693,6 +686,7 @@
                 mOverflowBubbles.remove(b);
                 mStateChange.bubbleRemoved(b, reason);
                 mStateChange.removedOverflowBubble = b;
+                mStateChange.showOverflowChanged = mOverflowBubbles.isEmpty();
             }
             if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) {
                 Bubble b = getSuppressedBubbleWithKey(key);
@@ -792,6 +786,9 @@
         }
         ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey());
         mLogger.logOverflowAdd(bubble, reason);
+        if (mOverflowBubbles.isEmpty()) {
+            mStateChange.showOverflowChanged = true;
+        }
         mOverflowBubbles.remove(bubble);
         mOverflowBubbles.add(0, bubble);
         mStateChange.addedOverflowBubble = bubble;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 3c788b1..be88b34 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -536,7 +536,8 @@
     private OnClickListener mBubbleClickListener = new OnClickListener() {
         @Override
         public void onClick(View view) {
-            mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging.
+            // If the touch ended in a click, we're no longer dragging.
+            onDraggingEnded();
 
             // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we
             // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust
@@ -719,8 +720,7 @@
                 mDismissView.hide();
             }
 
-            mIsDraggingStack = false;
-            mMagnetizedObject = null;
+            onDraggingEnded();
 
             // Hide the stack after a delay, if needed.
             updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
@@ -1096,6 +1096,7 @@
             } else {
                 maybeShowStackEdu();
             }
+            onDraggingEnded();
         });
 
         animate()
@@ -1153,6 +1154,14 @@
     }
 
     /**
+     * Reset state related to dragging.
+     */
+    private void onDraggingEnded() {
+        mIsDraggingStack = false;
+        mMagnetizedObject = null;
+    }
+
+    /**
      * Sets whether or not the stack should become temporarily invisible by moving off the side of
      * the screen.
      *
@@ -2341,8 +2350,8 @@
 
         showScrim(true, null /* runnable */);
         updateBubbleShadows(mIsExpanded);
-        updateBadges(false /* setBadgeForCollapsedStack */);
         mBubbleContainer.setActiveController(mExpandedAnimationController);
+        updateBadges(false /* setBadgeForCollapsedStack */);
         updateOverflowVisibility();
         updatePointerPosition(false /* forIme */);
         mExpandedAnimationController.expandFromStack(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
index 3b3974d..651bf02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
@@ -22,7 +22,6 @@
 import android.view.LayoutInflater
 import android.view.View
 import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
 import androidx.core.view.updateLayoutParams
 import com.android.wm.shell.R
 import com.android.wm.shell.bubbles.BubblePositioner
@@ -79,7 +78,11 @@
 
     override fun updateLocation(location: BubbleBarLocation) {
         val view = dropTargetView ?: return
-        getBounds(location.isOnLeft(view.isLayoutRtl), tempRect)
+        positioner.getBubbleBarExpandedViewBounds(
+            location.isOnLeft(view.isLayoutRtl),
+            false /* isOverflowExpanded */,
+            tempRect
+        )
         view.updateLayoutParams<FrameLayout.LayoutParams> {
             width = tempRect.width()
             height = tempRect.height()
@@ -87,17 +90,4 @@
         view.x = tempRect.left.toFloat()
         view.y = tempRect.top.toFloat()
     }
-
-    private fun getBounds(onLeft: Boolean, out: Rect) {
-        positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOverflowExpanded */, out)
-        val centerX = out.centerX()
-        val centerY = out.centerY()
-        out.scale(DROP_TARGET_SCALE)
-        // Move rect center back to the same position as before scale
-        out.offset(centerX - out.centerX(), centerY - out.centerY())
-    }
-
-    companion object {
-        @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index 98dccbb..da414cc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -389,9 +389,6 @@
         public void dispatchDragEvent(DragEvent event) {}
 
         @Override
-        public void updatePointerIcon(float x, float y) {}
-
-        @Override
         public void dispatchWindowShown() {}
 
         @Override
@@ -409,5 +406,10 @@
                 // ignore
             }
         }
+
+        @Override
+        public void dumpWindow(ParcelFileDescriptor pfd) {
+
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
index e5f6c37..ec3c601 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Point;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -49,6 +50,10 @@
     public String unsupressedBubbleKey;
     @Nullable
     public BubbleBarLocation bubbleBarLocation;
+    @Nullable
+    public Point expandedViewDropTargetSize;
+    public boolean showOverflowChanged;
+    public boolean showOverflow;
 
     // This is only populated if bubbles have been removed.
     public List<RemovedBubble> removedBubbles = new ArrayList<>();
@@ -81,12 +86,16 @@
         suppressedBubbleKey = parcel.readString();
         unsupressedBubbleKey = parcel.readString();
         removedBubbles = parcel.readParcelableList(new ArrayList<>(),
-                RemovedBubble.class.getClassLoader());
+                RemovedBubble.class.getClassLoader(), RemovedBubble.class);
         parcel.readStringList(bubbleKeysInOrder);
         currentBubbleList = parcel.readParcelableList(new ArrayList<>(),
-                BubbleInfo.class.getClassLoader());
+                BubbleInfo.class.getClassLoader(), BubbleInfo.class);
         bubbleBarLocation = parcel.readParcelable(BubbleBarLocation.class.getClassLoader(),
                 BubbleBarLocation.class);
+        expandedViewDropTargetSize = parcel.readParcelable(Point.class.getClassLoader(),
+                Point.class);
+        showOverflowChanged = parcel.readBoolean();
+        showOverflow = parcel.readBoolean();
     }
 
     /**
@@ -102,9 +111,11 @@
                 || suppressedBubbleKey != null
                 || unsupressedBubbleKey != null
                 || !currentBubbleList.isEmpty()
-                || bubbleBarLocation != null;
+                || bubbleBarLocation != null
+                || showOverflowChanged;
     }
 
+    @NonNull
     @Override
     public String toString() {
         return "BubbleBarUpdate{"
@@ -121,6 +132,9 @@
                 + " bubbles=" + bubbleKeysInOrder
                 + " currentBubbleList=" + currentBubbleList
                 + " bubbleBarLocation=" + bubbleBarLocation
+                + " expandedViewDropTargetSize=" + expandedViewDropTargetSize
+                + " showOverflowChanged=" + showOverflowChanged
+                + " showOverflow=" + showOverflow
                 + " }";
     }
 
@@ -144,6 +158,9 @@
         parcel.writeStringList(bubbleKeysInOrder);
         parcel.writeParcelableList(currentBubbleList, flags);
         parcel.writeParcelable(bubbleBarLocation, flags);
+        parcel.writeParcelable(expandedViewDropTargetSize, flags);
+        parcel.writeBoolean(showOverflowChanged);
+        parcel.writeBoolean(showOverflow);
     }
 
     /**
@@ -157,10 +174,11 @@
 
     @NonNull
     public static final Creator<BubbleBarUpdate> CREATOR =
-            new Creator<BubbleBarUpdate>() {
+            new Creator<>() {
                 public BubbleBarUpdate createFromParcel(Parcel source) {
                     return new BubbleBarUpdate(source);
                 }
+
                 public BubbleBarUpdate[] newArray(int size) {
                     return new BubbleBarUpdate[size];
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
new file mode 100644
index 0000000..08c7031
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module bubble owner
+madym@google.com
+atsjenk@google.com
+liranb@google.com
+sukeshram@google.com
+mpodolian@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
index 1e30d8f..dba0a98 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -16,11 +16,14 @@
 package com.android.wm.shell.common.pip
 
 import android.app.ActivityTaskManager
+import android.app.AppGlobals
 import android.app.RemoteAction
 import android.app.WindowConfiguration
 import android.content.ComponentName
 import android.content.Context
+import android.content.pm.PackageManager
 import android.os.RemoteException
+import android.os.SystemProperties
 import android.util.DisplayMetrics
 import android.util.Log
 import android.util.Pair
@@ -135,7 +138,23 @@
         }
     }
 
+    private var isPip2ExperimentEnabled: Boolean? = null
+
+    /**
+     * Returns true if PiP2 implementation should be used. Besides the trunk stable flag,
+     * system property can be used to override this read only flag during development.
+     * It's currently limited to phone form factor, i.e., not enabled on ARC / TV.
+     */
     @JvmStatic
-    val isPip2ExperimentEnabled: Boolean
-        get() = Flags.enablePip2Implementation()
+    fun isPip2ExperimentEnabled(): Boolean {
+        if (isPip2ExperimentEnabled == null) {
+            val isArc = AppGlobals.getPackageManager().hasSystemFeature(
+                "org.chromium.arc", 0)
+            val isTv = AppGlobals.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_LEANBACK, 0)
+            isPip2ExperimentEnabled = SystemProperties.getBoolean("wm_shell.pip2", false) ||
+                    (Flags.enablePip2Implementation() && !isArc && !isTv)
+        }
+        return isPip2ExperimentEnabled as Boolean
+    }
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index f195f95..3ab1fad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -81,6 +81,10 @@
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
         mCallback = callback;
         mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
+        if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
+            // Don't show the SCM button for freeform tasks
+            mHasSizeCompat &= !taskInfo.isFreeform();
+        }
         mCameraCompatControlState =
                 taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
         mCompatUIHintsState = compatUIHintsState;
@@ -136,6 +140,10 @@
         final boolean prevHasSizeCompat = mHasSizeCompat;
         final int prevCameraCompatControlState = mCameraCompatControlState;
         mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
+        if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
+            // Don't show the SCM button for freeform tasks
+            mHasSizeCompat &= !taskInfo.isFreeform();
+        }
         mCameraCompatControlState =
                 taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 17121c8..e729c7d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -891,13 +891,13 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopTasksController> providesDesktopTasksController(
+    static Optional<DesktopTasksController> providesDesktopTasksController(Context context,
             @DynamicOverride Optional<Lazy<DesktopTasksController>> desktopTasksController) {
         // Use optional-of-lazy for the dependency that this provider relies on.
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
         return desktopTasksController.flatMap((lazy)-> {
-            if (DesktopModeStatus.isEnabled()) {
+            if (DesktopModeStatus.canEnterDesktopMode(context)) {
                 return Optional.of(lazy.get());
             }
             return Optional.empty();
@@ -910,13 +910,13 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(
+    static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(Context context,
             @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) {
         // Use optional-of-lazy for the dependency that this provider relies on.
         // Lazy ensures that this provider will not be the cause the dependency is created
         // when it will not be returned due to the condition below.
         return desktopModeTaskRepository.flatMap((lazy)-> {
-            if (DesktopModeStatus.isEnabled()) {
+            if (DesktopModeStatus.canEnterDesktopMode(context)) {
                 return Optional.of(lazy.get());
             }
             return Optional.empty();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b574b81..a1910c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -221,7 +221,7 @@
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return new DesktopModeWindowDecorViewModel(
                     context,
                     mainExecutor,
@@ -278,8 +278,8 @@
         ShellInit init = FreeformComponents.isFreeformEnabled(context)
                 ? shellInit
                 : null;
-        return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository,
-                windowDecorViewModel);
+        return new FreeformTaskListener(context, init, shellTaskOrganizer,
+                desktopModeTaskRepository, windowDecorViewModel);
     }
 
     @WMSingleton
@@ -535,10 +535,12 @@
     @WMSingleton
     @Provides
     static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter(
+            Context context,
             Transitions transitions,
             @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
             ShellTaskOrganizer shellTaskOrganizer) {
-        if (!DesktopModeStatus.isEnabled() || !Flags.enableDesktopWindowingTaskLimit()) {
+        if (!DesktopModeStatus.canEnterDesktopMode(context)
+                || !Flags.enableDesktopWindowingTaskLimit()) {
             return Optional.empty();
         }
         return Optional.of(
@@ -592,23 +594,26 @@
     @WMSingleton
     @Provides
     static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver(
+            Context context,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
             Transitions transitions,
             ShellInit shellInit
     ) {
         return desktopModeTaskRepository.flatMap(repository ->
-                Optional.of(new DesktopTasksTransitionObserver(repository, transitions, shellInit))
+                Optional.of(new DesktopTasksTransitionObserver(
+                        context, repository, transitions, shellInit))
         );
     }
 
     @WMSingleton
     @Provides
     static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
+            Context context,
             ShellInit shellInit,
             Transitions transitions,
             DesktopModeEventLogger desktopModeEventLogger) {
         return new DesktopModeLoggerTransitionObserver(
-                shellInit, transitions, desktopModeEventLogger);
+                context, shellInit, transitions, desktopModeEventLogger);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index a10c7c0..9038aaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -20,6 +20,7 @@
 import android.app.ActivityTaskManager.INVALID_TASK_ID
 import android.app.TaskInfo
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
 import android.os.IBinder
 import android.util.SparseArray
 import android.view.SurfaceControl
@@ -49,6 +50,7 @@
  * and other transitions that originate both within and outside shell.
  */
 class DesktopModeLoggerTransitionObserver(
+    context: Context,
     shellInit: ShellInit,
     private val transitions: Transitions,
     private val desktopModeEventLogger: DesktopModeEventLogger
@@ -57,7 +59,8 @@
     private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) }
 
     init {
-        if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS &&
+            DesktopModeStatus.canEnterDesktopMode(context)) {
             shellInit.addInitCallback(this::onInit, this)
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index fcddcad..b41454d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -65,7 +65,7 @@
             "persist.wm.debug.desktop_use_window_shadows_focused_window", false);
 
     /**
-     * Flag to indicate whether to apply shadows to windows in desktop mode.
+     * Flag to indicate whether to use rounded corners for windows in desktop mode.
      */
     private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean(
             "persist.wm.debug.desktop_use_rounded_corners", true);
@@ -93,8 +93,10 @@
             "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT);
 
     /**
-     * Return {@code true} if desktop windowing is enabled
+     * Return {@code true} if desktop windowing is enabled. Only to be used for testing. Callers
+     * should use {@link #canEnterDesktopMode(Context)} to query the state of desktop windowing.
      */
+    @VisibleForTesting
     public static boolean isEnabled() {
         return Flags.enableDesktopWindowingMode();
     }
@@ -155,9 +157,9 @@
     }
 
     /**
-     * Return {@code true} if desktop mode can be entered on the current device.
+     * Return {@code true} if desktop mode is enabled and can be entered on the current device.
      */
     public static boolean canEnterDesktopMode(@NonNull Context context) {
-        return !enforceDeviceRestrictions() || isDesktopModeSupported(context);
+        return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
new file mode 100644
index 0000000..6da3741
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("DesktopModeUtils")
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.pm.ActivityInfo.isFixedOrientationLandscape
+import android.content.pm.ActivityInfo.isFixedOrientationPortrait
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.graphics.Rect
+import android.os.SystemProperties
+import android.util.Size
+import com.android.wm.shell.common.DisplayLayout
+
+
+val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float = SystemProperties
+        .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
+
+val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int = SystemProperties
+        .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)
+
+
+/**
+ * Calculates the initial bounds required for an application to fill a scale of the display bounds
+ * without any letterboxing. This is done by taking into account the applications fullscreen size,
+ * aspect ratio, orientation and resizability to calculate an area this is compatible with the
+ * applications previous configuration.
+ */
+fun calculateInitialBounds(
+    displayLayout: DisplayLayout,
+    taskInfo: RunningTaskInfo,
+    scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE
+): Rect {
+    val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
+    val appAspectRatio = calculateAspectRatio(taskInfo)
+    val idealSize = calculateIdealSize(screenBounds, scale)
+    // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
+    // Instead default to the desired initial bounds.
+    val topActivityInfo = taskInfo.topActivityInfo
+        ?: return positionInScreen(idealSize, screenBounds)
+
+    val initialSize: Size = when (taskInfo.configuration.orientation) {
+        ORIENTATION_LANDSCAPE -> {
+            if (taskInfo.isResizeable) {
+                if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
+                    // Respect apps fullscreen width
+                    Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height)
+                } else {
+                    idealSize
+                }
+            } else {
+                maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
+                    appAspectRatio)
+            }
+        }
+        ORIENTATION_PORTRAIT -> {
+            val customPortraitWidthForLandscapeApp = screenBounds.width() -
+                    (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
+            if (taskInfo.isResizeable) {
+                if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+                    // Respect apps fullscreen height and apply custom app width
+                    Size(customPortraitWidthForLandscapeApp,
+                        taskInfo.appCompatTaskInfo.topActivityLetterboxHeight)
+                } else {
+                    idealSize
+                }
+            } else {
+                if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+                    // Apply custom app width and calculate maximum size
+                    maximumSizeMaintainingAspectRatio(
+                        taskInfo,
+                        Size(customPortraitWidthForLandscapeApp, idealSize.height),
+                        appAspectRatio)
+                } else {
+                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
+                        appAspectRatio)
+                }
+            }
+        }
+        else -> {
+            idealSize
+        }
+    }
+
+    return positionInScreen(initialSize, screenBounds)
+}
+
+/**
+ * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+ * ratio.
+ */
+private fun maximumSizeMaintainingAspectRatio(
+    taskInfo: RunningTaskInfo,
+    targetArea: Size,
+    aspectRatio: Float
+): Size {
+    val targetHeight = targetArea.height
+    val targetWidth = targetArea.width
+    val finalHeight: Int
+    val finalWidth: Int
+    if (isFixedOrientationPortrait(taskInfo.topActivityInfo!!.screenOrientation)) {
+        val tempWidth = (targetHeight / aspectRatio).toInt()
+        if (tempWidth <= targetWidth) {
+            finalHeight = targetHeight
+            finalWidth = tempWidth
+        } else {
+            finalWidth = targetWidth
+            finalHeight = (finalWidth * aspectRatio).toInt()
+        }
+    } else {
+        val tempWidth = (targetHeight * aspectRatio).toInt()
+        if (tempWidth <= targetWidth) {
+            finalHeight = targetHeight
+            finalWidth = tempWidth
+        } else {
+            finalWidth = targetWidth
+            finalHeight = (finalWidth / aspectRatio).toInt()
+        }
+    }
+    return Size(finalWidth, finalHeight)
+}
+
+/**
+ * Calculates the aspect ratio of an activity from its fullscreen bounds.
+ */
+private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float {
+    if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
+        val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth
+        val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
+        return maxOf(appLetterboxWidth, appLetterboxHeight) /
+                minOf(appLetterboxWidth, appLetterboxHeight).toFloat()
+    }
+    val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f
+    return maxOf(appBounds.height(), appBounds.width()) /
+                minOf(appBounds.height(), appBounds.width()).toFloat()
+}
+
+/**
+ * Calculates the desired initial bounds for applications in desktop windowing. This is done as a
+ * scale of the screen bounds.
+ */
+private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size {
+    val width = (screenBounds.width() * scale).toInt()
+    val height = (screenBounds.height() * scale).toInt()
+    return Size(width, height)
+}
+
+/**
+ * Adjusts bounds to be positioned in the middle of the screen.
+ */
+private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
+    // TODO(b/325240051): Position apps with bottom heavy offset
+    val heightOffset = (screenBounds.height() - desiredSize.height) / 2
+    val widthOffset = (screenBounds.width() - desiredSize.width) / 2
+    return Rect(widthOffset, heightOffset,
+        desiredSize.width + widthOffset, desiredSize.height + heightOffset)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index ecfb134..b0d5923 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -47,6 +47,7 @@
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import androidx.annotation.BinderThread
+import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.window.flags.Flags
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -85,7 +86,6 @@
 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
-import com.android.wm.shell.windowdecor.extension.isFreeform
 import com.android.wm.shell.windowdecor.extension.isFullscreen
 import java.io.PrintWriter
 import java.util.Optional
@@ -167,7 +167,7 @@
 
     init {
         desktopMode = DesktopModeImpl()
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(context)) {
             shellInit.addInitCallback({ onInit() }, this)
         }
     }
@@ -203,6 +203,11 @@
         dragAndDropController.addListener(this)
     }
 
+    @VisibleForTesting
+    fun getVisualIndicator(): DesktopModeVisualIndicator? {
+        return visualIndicator
+    }
+
     fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
         toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
@@ -605,8 +610,9 @@
     }
 
     /**
-     * Quick-resizes a desktop task, toggling between the stable bounds and the last saved bounds
-     * if available or the default bounds otherwise.
+     * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the
+     * stable bounds) and a free floating state (either the last saved bounds if available or the
+     * default bounds otherwise).
      */
     fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -623,7 +629,11 @@
             if (taskBoundsBeforeMaximize != null) {
                 destinationBounds.set(taskBoundsBeforeMaximize)
             } else {
-                destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
+                if (Flags.enableWindowingDynamicInitialBounds()){
+                    destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo))
+                } else {
+                    destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
+                }
             }
         } else {
             // Save current bounds so that task can be restored back to original bounds if necessary
@@ -1011,6 +1021,7 @@
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
+        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
@@ -1019,6 +1030,9 @@
         } else {
             WINDOWING_MODE_FREEFORM
         }
+        if (Flags.enableWindowingDynamicInitialBounds()) {
+            wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
+        }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
         wct.reorder(taskInfo.token, true /* onTop */)
         if (isDesktopDensityOverrideSet()) {
@@ -1239,13 +1253,17 @@
      * @param y height of drag, to be checked against status bar height.
      */
     fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
-        val indicator = visualIndicator ?: return
+        val indicator = getVisualIndicator() ?: return
         val indicatorType = indicator
             .updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
         when (indicatorType) {
             DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
                 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
-                finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
+                if (Flags.enableWindowingDynamicInitialBounds()) {
+                    finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
+                } else {
+                    finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
+                }
             }
             DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR,
             DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 20df264..451e09c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.content.Context
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager
@@ -33,13 +34,15 @@
  * and other transitions that originate both within and outside shell.
  */
 class DesktopTasksTransitionObserver(
+    context: Context,
     private val desktopModeTaskRepository: DesktopModeTaskRepository,
     private val transitions: Transitions,
     shellInit: ShellInit
 ) : Transitions.TransitionObserver {
 
     init {
-        if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) {
+        if (Transitions.ENABLE_SHELL_TRANSITIONS &&
+            DesktopModeStatus.canEnterDesktopMode(context)) {
             shellInit.addInitCallback(::onInit, this)
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 6fea203..a414a55 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -21,6 +21,7 @@
 import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM;
 
 import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
 
@@ -44,6 +45,7 @@
         ShellTaskOrganizer.FocusListener {
     private static final String TAG = "FreeformTaskListener";
 
+    private final Context mContext;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
     private final WindowDecorViewModel mWindowDecorationViewModel;
@@ -56,10 +58,12 @@
     }
 
     public FreeformTaskListener(
+            Context context,
             ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
             WindowDecorViewModel windowDecorationViewModel) {
+        mContext = context;
         mShellTaskOrganizer = shellTaskOrganizer;
         mWindowDecorationViewModel = windowDecorationViewModel;
         mDesktopModeTaskRepository = desktopModeTaskRepository;
@@ -70,7 +74,7 @@
 
     private void onInit() {
         mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mShellTaskOrganizer.addFocusListener(this);
         }
     }
@@ -92,7 +96,7 @@
             t.apply();
         }
 
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
@@ -114,7 +118,7 @@
                 taskInfo.taskId);
         mTasks.remove(taskInfo.taskId);
 
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 repository.removeFreeformTask(taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
@@ -125,7 +129,7 @@
                 repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false);
             });
         }
-
+        mWindowDecorationViewModel.onTaskVanished(taskInfo);
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
             mWindowDecorationViewModel.destroyWindowDecoration(taskInfo);
         }
@@ -139,7 +143,7 @@
                 taskInfo.taskId);
         mWindowDecorationViewModel.onTaskInfoChanged(taskInfo);
         state.mTaskInfo = taskInfo;
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 if (taskInfo.isVisible) {
                     if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
@@ -161,7 +165,7 @@
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG,
                 "Freeform Task Focus Changed: #%d focused=%b",
                 taskInfo.taskId, taskInfo.isFocused);
-        if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
                 repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 998728d6..2626e73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -161,7 +161,7 @@
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
                 taskInfo.taskId);
         mTasks.remove(taskInfo.taskId);
-
+        mWindowDecorViewModelOptional.ifPresent(v -> v.onTaskVanished(taskInfo));
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
         if (mWindowDecorViewModelOptional.isPresent()) {
             mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index ad29d15..19af3d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -52,7 +52,7 @@
     WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
     WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
-            Consts.TAG_WM_SHELL),
+            Consts.TAG_WM_DESKTOP_MODE),
     WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
     WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
@@ -120,6 +120,7 @@
         private static final String TAG_WM_SHELL = "WindowManagerShell";
         private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow";
         private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen";
+        private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode";
 
         private static final boolean ENABLE_DEBUG = true;
         private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index c5f23a8..a8611d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -327,7 +327,8 @@
 
     private boolean shouldEnableRunningTasksForDesktopMode() {
         return mPcFeatureEnabled
-                || (DesktopModeStatus.isEnabled() && enableDesktopWindowingTaskbarRunningApps());
+                || (DesktopModeStatus.canEnterDesktopMode(mContext)
+                && enableDesktopWindowingTaskbarRunningApps());
     }
 
     @VisibleForTesting
@@ -371,7 +372,8 @@
                 continue;
             }
 
-            if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent()
+            if (DesktopModeStatus.canEnterDesktopMode(mContext)
+                    && mDesktopModeTaskRepository.isPresent()
                     && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
                 if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
                     // Minimized freeform tasks should not be shown at all.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index f41bca3..1e305c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -260,6 +260,7 @@
     public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId);
         final int taskId = taskInfo.taskId;
+        mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo));
         if (mRootTaskInfo.taskId == taskId) {
             mCallbacks.onRootTaskVanished();
             mRootTaskInfo = null;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 4d02ec2..968b27b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -426,7 +426,8 @@
                     ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                             "Converting mixed transition into a keyguard transition");
                     // Consume the original mixed transition
-                    onTransitionConsumed(transition, false, null);
+                    mActiveTransitions.remove(mixed);
+                    mixed.onTransitionConsumed(transition, false, null);
                     return true;
                 } else {
                     // Keyguard handler cannot handle it, process through original mixed
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 87dc391..e85cb64 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
@@ -119,6 +119,21 @@
     }
 
     @Override
+    public void onTaskVanished(RunningTaskInfo taskInfo) {
+        // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
+        // windowing mode changed. We're only interested in closing tasks so checking whether
+        // its info still exists in the task organizer is one way to disambiguate.
+        final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
+        if (closed) {
+            // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
+            // changes happen, but there are certain cases in which closing tasks aren't included
+            // in transitions, such as when a non-visible task is closed. See b/296921167.
+            // Destroy the decoration here in case the lack of transition missed it.
+            destroyWindowDecoration(taskInfo);
+        }
+    }
+
+    @Override
     public void onTaskChanging(
             RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
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 01175f5..dfdb58a 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
@@ -35,6 +35,7 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent;
 import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 
 import android.annotation.NonNull;
@@ -72,6 +73,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.window.flags.Flags;
 import com.android.wm.shell.R;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
@@ -276,9 +278,11 @@
             public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
                 if (visible && stage != STAGE_TYPE_UNDEFINED) {
                     DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
-                    if (decor != null && DesktopModeStatus.isEnabled()) {
-                        mDesktopTasksController.moveToSplit(decor.mTaskInfo);
+                    if (decor == null || !DesktopModeStatus.canEnterDesktopMode(mContext)
+                            || decor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+                        return;
                     }
+                    mDesktopTasksController.moveToSplit(decor.mTaskInfo);
                 }
             }
         });
@@ -309,6 +313,22 @@
     }
 
     @Override
+    public void onTaskVanished(RunningTaskInfo taskInfo) {
+        // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
+        // windowing mode changed. We're only interested in closing tasks so checking whether
+        // its info still exists in the task organizer is one way to disambiguate.
+        final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
+        ProtoLog.v(WM_SHELL_DESKTOP_MODE, "Task Vanished: #%d closed=%b", taskInfo.taskId, closed);
+        if (closed) {
+            // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
+            // changes happen, but there are certain cases in which closing tasks aren't included
+            // in transitions, such as when a non-visible task is closed. See b/296921167.
+            // Destroy the decoration here in case the lack of transition missed it.
+            destroyWindowDecoration(taskInfo);
+        }
+    }
+
+    @Override
     public void onTaskChanging(
             RunningTaskInfo taskInfo,
             SurfaceControl taskSurface,
@@ -594,7 +614,7 @@
         public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
             final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-            if (DesktopModeStatus.isEnabled()
+            if (DesktopModeStatus.canEnterDesktopMode(mContext)
                     && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
                 return false;
             }
@@ -771,7 +791,7 @@
      */
     private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
         final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev);
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             if (!mInImmersiveMode && (relevantDecor == null
                     || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM
                     || mTransitionDragActive)) {
@@ -780,7 +800,7 @@
         }
         handleEventOutsideCaption(ev, relevantDecor);
         // Prevent status bar from reacting to a caption drag.
-        if (DesktopModeStatus.isEnabled()) {
+        if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             if (mTransitionDragActive) {
                 inputMonitor.pilferPointers();
             }
@@ -838,7 +858,7 @@
                 mDragToDesktopAnimationStartBounds.set(
                         relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
                 boolean dragFromStatusBarAllowed = false;
-                if (DesktopModeStatus.isEnabled()) {
+                if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
                     // In proto2 any full screen or multi-window task can be dragged to
                     // freeform.
                     final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
@@ -1013,12 +1033,11 @@
                 && isSingleTopActivityTranslucent(taskInfo)) {
             return false;
         }
-        return DesktopModeStatus.isEnabled()
+        return DesktopModeStatus.canEnterDesktopMode(mContext)
                 && !DesktopWallpaperActivity.isWallpaperTask(taskInfo)
                 && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
                 && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
-                && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop()
-                && DesktopModeStatus.canEnterDesktopMode(mContext);
+                && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop();
     }
 
     private void createWindowDecoration(
@@ -1087,7 +1106,8 @@
     private void dump(PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         pw.println(prefix + "DesktopModeWindowDecorViewModel");
-        pw.println(innerPrefix + "DesktopModeStatus=" + DesktopModeStatus.isEnabled());
+        pw.println(innerPrefix + "DesktopModeStatus="
+                + DesktopModeStatus.canEnterDesktopMode(mContext));
         pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive);
         pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay);
         pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId);
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 8c6bc73..4c347ad 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
@@ -110,6 +110,8 @@
 
     private ResizeVeil mResizeVeil;
     private Bitmap mAppIconBitmap;
+    private Bitmap mResizeVeilBitmap;
+
     private CharSequence mAppName;
 
     private ExclusionRegionListener mExclusionRegionListener;
@@ -468,11 +470,15 @@
             PackageManager pm = mContext.getApplicationContext().getPackageManager();
             final IconProvider provider = new IconProvider(mContext);
             final Drawable appIconDrawable = provider.getIcon(activityInfo);
-            final Resources resources = mContext.getResources();
-            final BaseIconFactory factory = new BaseIconFactory(mContext,
-                    resources.getDisplayMetrics().densityDpi,
-                    resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
-            mAppIconBitmap = factory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+            final BaseIconFactory headerIconFactory = createIconFactory(mContext,
+                    R.dimen.desktop_mode_caption_icon_radius);
+            mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+
+            final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext,
+                    R.dimen.desktop_mode_resize_veil_icon_size);
+            mResizeVeilBitmap = resizeVeilIconFactory
+                    .createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+
             final ApplicationInfo applicationInfo = activityInfo.applicationInfo;
             mAppName = pm.getApplicationLabel(applicationInfo);
         } finally {
@@ -480,6 +486,13 @@
         }
     }
 
+    private BaseIconFactory createIconFactory(Context context, int dimensions) {
+        final Resources resources = context.getResources();
+        final int densityDpi = resources.getDisplayMetrics().densityDpi;
+        final int iconSize = resources.getDimensionPixelSize(dimensions);
+        return new BaseIconFactory(context, densityDpi, iconSize);
+    }
+
     private void closeDragResizeListener() {
         if (mDragResizeListener == null) {
             return;
@@ -495,7 +508,7 @@
     private void createResizeVeilIfNeeded() {
         if (mResizeVeil != null) return;
         loadAppInfoIfNeeded();
-        mResizeVeil = new ResizeVeil(mContext, mDisplayController, mAppIconBitmap, mTaskInfo,
+        mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, mTaskInfo,
                 mTaskSurface, mSurfaceControlTransactionSupplier);
     }
 
@@ -638,7 +651,7 @@
                 .setOnClickListener(mOnCaptionButtonClickListener)
                 .setOnTouchListener(mOnCaptionTouchListener)
                 .setLayoutId(mRelayoutParams.mLayoutResId)
-                .setWindowingButtonsVisible(DesktopModeStatus.isEnabled())
+                .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext))
                 .setCaptionHeight(mResult.mCaptionHeight)
                 .build();
         mWindowDecorViewHolder.onHandleMenuOpened();
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 9624d46..5379ca6 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
@@ -24,7 +24,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER;
 
-import static com.android.input.flags.Flags.enablePointerChoreographer;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
 import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
@@ -54,6 +54,7 @@
 import android.view.WindowManagerGlobal;
 import android.window.InputTransferToken;
 
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
 
@@ -399,12 +400,17 @@
                         float rawX = e.getRawX(0);
                         float rawY = e.getRawY(0);
                         int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, x, y);
+                        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                                "%s: Handling action down, update ctrlType to %d", TAG, ctrlType);
                         mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
                                 rawX, rawY);
                         // Increase the input sink region to cover the whole screen; this is to
                         // prevent input and focus from going to other tasks during a drag resize.
                         updateInputSinkRegionForDrag(mDragStartTaskBounds);
                         result = true;
+                    } else {
+                        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                                "%s: Handling action down, but ignore event", TAG);
                     }
                     break;
                 }
@@ -499,12 +505,10 @@
             // where views in the task can receive input events because we can't set touch regions
             // of input sinks to have rounded corners.
             if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) {
-                if (enablePointerChoreographer()) {
-                    mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType),
-                            displayId, deviceId, pointerId, mInputChannel.getToken());
-                } else {
-                    mInputManager.setPointerIconType(cursorType);
-                }
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: update pointer icon from %d to %d",
+                        TAG, mLastCursorType, cursorType);
+                mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType),
+                        displayId, deviceId, pointerId, mInputChannel.getToken());
                 mLastCursorType = cursorType;
             }
         }
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 eafb569..4f513f0 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
@@ -33,6 +33,9 @@
 import android.util.Size;
 import android.view.MotionEvent;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.wm.shell.R;
 
 import java.util.Objects;
@@ -41,6 +44,11 @@
  * Geometry for a drag resize region for a particular window.
  */
 final class DragResizeWindowGeometry {
+    // TODO(b/337264971) clean up when no longer needed
+    @VisibleForTesting static final boolean DEBUG = true;
+    // The additional width to apply to edge resize bounds just for logging when a touch is
+    // close.
+    @VisibleForTesting static final int EDGE_DEBUG_BUFFER = 15;
     private final int mTaskCornerRadius;
     private final Size mTaskSize;
     // The size of the handle applied to the edges of the window, for the user to drag resize.
@@ -51,10 +59,9 @@
     // The task corners to permit drag resizing with a fine input, such as stylus or cursor.
     private final @NonNull TaskCorners mFineTaskCorners;
     // The bounds for each edge drag region, which can resize the task in one direction.
-    private final @NonNull Rect mTopEdgeBounds;
-    private final @NonNull Rect mLeftEdgeBounds;
-    private final @NonNull Rect mRightEdgeBounds;
-    private final @NonNull Rect mBottomEdgeBounds;
+    private final @NonNull TaskEdges mTaskEdges;
+    // Extra-large edge bounds for logging to help debug when an edge resize is ignored.
+    private final @Nullable TaskEdges mDebugTaskEdges;
 
     DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
             int resizeHandleThickness, int fineCornerSize, int largeCornerSize) {
@@ -66,26 +73,12 @@
         mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
 
         // Save touch areas for each edge.
-        mTopEdgeBounds = new Rect(
-                -mResizeHandleThickness,
-                -mResizeHandleThickness,
-                mTaskSize.getWidth() + mResizeHandleThickness,
-                0);
-        mLeftEdgeBounds = new Rect(
-                -mResizeHandleThickness,
-                0,
-                0,
-                mTaskSize.getHeight());
-        mRightEdgeBounds = new Rect(
-                mTaskSize.getWidth(),
-                0,
-                mTaskSize.getWidth() + mResizeHandleThickness,
-                mTaskSize.getHeight());
-        mBottomEdgeBounds = new Rect(
-                -mResizeHandleThickness,
-                mTaskSize.getHeight(),
-                mTaskSize.getWidth() + mResizeHandleThickness,
-                mTaskSize.getHeight() + mResizeHandleThickness);
+        mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness);
+        if (DEBUG) {
+            mDebugTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness + EDGE_DEBUG_BUFFER);
+        } else {
+            mDebugTaskEdges = null;
+        }
     }
 
     /**
@@ -127,10 +120,13 @@
      */
     void union(@NonNull Region region) {
         // Apply the edge resize regions.
-        region.union(mTopEdgeBounds);
-        region.union(mLeftEdgeBounds);
-        region.union(mRightEdgeBounds);
-        region.union(mBottomEdgeBounds);
+        if (inDebugMode()) {
+            // Use the larger edge sizes if we are debugging, to be able to log if we ignored a
+            // touch due to the size of the edge region.
+            mDebugTaskEdges.union(region);
+        } else {
+            mTaskEdges.union(region);
+        }
 
         if (enableWindowingEdgeDragResize()) {
             // Apply the corners as well for the larger corners, to ensure we capture all possible
@@ -216,6 +212,10 @@
 
     @DragPositioningCallback.CtrlType
     private int calculateEdgeResizeCtrlType(float x, float y) {
+        if (inDebugMode() && (mDebugTaskEdges.contains((int) x, (int) y)
+                    && !mTaskEdges.contains((int) x, (int) y))) {
+            return CTRL_TYPE_UNDEFINED;
+        }
         int ctrlType = CTRL_TYPE_UNDEFINED;
         // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
         // sides will use the bounds specified in setGeometry and not go into task bounds.
@@ -306,10 +306,9 @@
                 && this.mResizeHandleThickness == other.mResizeHandleThickness
                 && this.mFineTaskCorners.equals(other.mFineTaskCorners)
                 && this.mLargeTaskCorners.equals(other.mLargeTaskCorners)
-                && this.mTopEdgeBounds.equals(other.mTopEdgeBounds)
-                && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds)
-                && this.mRightEdgeBounds.equals(other.mRightEdgeBounds)
-                && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds);
+                && (inDebugMode()
+                        ? this.mDebugTaskEdges.equals(other.mDebugTaskEdges)
+                        : this.mTaskEdges.equals(other.mTaskEdges));
     }
 
     @Override
@@ -320,10 +319,11 @@
                 mResizeHandleThickness,
                 mFineTaskCorners,
                 mLargeTaskCorners,
-                mTopEdgeBounds,
-                mLeftEdgeBounds,
-                mRightEdgeBounds,
-                mBottomEdgeBounds);
+                (inDebugMode() ? mDebugTaskEdges : mTaskEdges));
+    }
+
+    private boolean inDebugMode() {
+        return DEBUG && mDebugTaskEdges != null;
     }
 
     /**
@@ -431,4 +431,92 @@
                     mRightBottomCornerBounds);
         }
     }
+
+    /**
+     * Representation of the drag resize regions at the edges of the window.
+     */
+    private static class TaskEdges {
+        private final @NonNull Rect mTopEdgeBounds;
+        private final @NonNull Rect mLeftEdgeBounds;
+        private final @NonNull Rect mRightEdgeBounds;
+        private final @NonNull Rect mBottomEdgeBounds;
+        private final @NonNull Region mRegion;
+
+        private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness) {
+            // Save touch areas for each edge.
+            mTopEdgeBounds = new Rect(
+                    -resizeHandleThickness,
+                    -resizeHandleThickness,
+                    taskSize.getWidth() + resizeHandleThickness,
+                    0);
+            mLeftEdgeBounds = new Rect(
+                    -resizeHandleThickness,
+                    0,
+                    0,
+                    taskSize.getHeight());
+            mRightEdgeBounds = new Rect(
+                    taskSize.getWidth(),
+                    0,
+                    taskSize.getWidth() + resizeHandleThickness,
+                    taskSize.getHeight());
+            mBottomEdgeBounds = new Rect(
+                    -resizeHandleThickness,
+                    taskSize.getHeight(),
+                    taskSize.getWidth() + resizeHandleThickness,
+                    taskSize.getHeight() + resizeHandleThickness);
+
+            mRegion = new Region();
+            mRegion.union(mTopEdgeBounds);
+            mRegion.union(mLeftEdgeBounds);
+            mRegion.union(mRightEdgeBounds);
+            mRegion.union(mBottomEdgeBounds);
+        }
+
+        /**
+         * Returns {@code true} if the edges contain the given point.
+         */
+        private boolean contains(int x, int y) {
+            return mRegion.contains(x, y);
+        }
+
+        /**
+         * Updates the region to include all four corners.
+         */
+        private void union(Region region) {
+            region.union(mTopEdgeBounds);
+            region.union(mLeftEdgeBounds);
+            region.union(mRightEdgeBounds);
+            region.union(mBottomEdgeBounds);
+        }
+
+        @Override
+        public String toString() {
+            return "TaskEdges for the"
+                    + " top " + mTopEdgeBounds
+                    + " left " + mLeftEdgeBounds
+                    + " right " + mRightEdgeBounds
+                    + " bottom " + mBottomEdgeBounds;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) return false;
+            if (this == obj) return true;
+            if (!(obj instanceof TaskEdges other)) return false;
+
+            return this.mTopEdgeBounds.equals(other.mTopEdgeBounds)
+                    && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds)
+                    && this.mRightEdgeBounds.equals(other.mRightEdgeBounds)
+                    && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(
+                    mTopEdgeBounds,
+                    mLeftEdgeBounds,
+                    mRightEdgeBounds,
+                    mBottomEdgeBounds);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 01a6012..1563259 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -67,6 +67,14 @@
     void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo);
 
     /**
+     * Notifies a task has vanished, which can mean that the task changed windowing mode or was
+     * removed.
+     *
+     * @param taskInfo the task info of the task
+     */
+    void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo);
+
+    /**
      * Notifies a transition is about to start about the given task to give the window decoration a
      * chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates
      * a window decoration if one does not exist but is required.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
index a2293d5..ec20471 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
@@ -17,7 +17,6 @@
 package com.android.wm.shell.windowdecor.extension
 
 import android.app.TaskInfo
-import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
 import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS
 import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
@@ -36,6 +35,3 @@
 
 val TaskInfo.isFullscreen: Boolean
     get() = windowingMode == WINDOWING_MODE_FULLSCREEN
-
-val TaskInfo.isFreeform: Boolean
-    get() = windowingMode == WINDOWING_MODE_FREEFORM
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 6be411d..0f43377 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -1193,23 +1193,6 @@
     }
 
     @Test
-    public void test_removeOverflowBubble() {
-        sendUpdatedEntryAtTime(mEntryA1, 2000);
-        mBubbleData.setListener(mListener);
-
-        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
-        verifyUpdateReceived();
-        assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
-
-        mBubbleData.removeOverflowBubble(mBubbleA1);
-        verifyUpdateReceived();
-
-        BubbleData.Update update = mUpdateCaptor.getValue();
-        assertThat(update.removedOverflowBubble).isEqualTo(mBubbleA1);
-        assertOverflowChangedTo(ImmutableList.of());
-    }
-
-    @Test
     public void test_getInitialStateForBubbleBar_includesInitialBubblesAndPosition() {
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -1235,6 +1218,51 @@
         assertExpandedChangedTo(true);
     }
 
+    @Test
+    public void testShowOverflowChanged_hasOverflowBubbles() {
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        verifyUpdateReceived();
+        assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+    }
+
+    @Test
+    public void testShowOverflowChanged_false_hasOverflowBubbles() {
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        sendUpdatedEntryAtTime(mEntryA2, 1000);
+        mBubbleData.setListener(mListener);
+
+        // First overflowed causes change event
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        verifyUpdateReceived();
+        assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+
+        // Second overflow does not
+        mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        verifyUpdateReceived();
+        assertThat(mUpdateCaptor.getValue().showOverflowChanged).isFalse();
+    }
+
+    @Test
+    public void testShowOverflowChanged_noOverflowBubbles() {
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+
+        verifyUpdateReceived();
+        assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+    }
+
     private void verifyUpdateReceived() {
         verify(mListener).applyUpdate(mUpdateCaptor.capture());
         reset(mListener);
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 65117f7..60a7dcd 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
@@ -18,6 +18,7 @@
 import android.app.ActivityManager
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.content.Context
 import android.os.IBinder
 import android.testing.AndroidTestingRunner
 import android.view.SurfaceControl
@@ -35,6 +36,7 @@
 import android.window.TransitionInfo.Change
 import android.window.WindowContainerToken
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.modules.utils.testing.ExtendedMockitoRule
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
@@ -58,6 +60,11 @@
 import org.mockito.kotlin.same
 import org.mockito.kotlin.times
 
+/**
+ * Test class for {@link DesktopModeLoggerTransitionObserver}
+ *
+ * Usage: atest WMShellUnitTests:DesktopModeLoggerTransitionObserverTest
+ */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class DesktopModeLoggerTransitionObserverTest {
@@ -74,6 +81,8 @@
     private lateinit var mockShellInit: ShellInit
     @Mock
     private lateinit var transitions: Transitions
+    @Mock
+    private lateinit var context: Context
 
     private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
     private lateinit var shellInit: ShellInit
@@ -81,12 +90,12 @@
 
     @Before
     fun setup() {
-        Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true)
+        doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
         shellInit = Mockito.spy(ShellInit(testExecutor))
         desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
 
         transitionObserver = DesktopModeLoggerTransitionObserver(
-            mockShellInit, transitions, desktopModeEventLogger)
+            context, mockShellInit, transitions, desktopModeEventLogger)
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             val initRunnableCaptor = ArgumentCaptor.forClass(
                 Runnable::class.java)
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 ad4b720..3f76c4f 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
@@ -24,6 +24,12 @@
 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
 import android.graphics.Point
 import android.graphics.PointF
 import android.graphics.Rect
@@ -101,7 +107,9 @@
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.anyOrNull
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.capture
 import org.mockito.quality.Strictness
@@ -141,6 +149,7 @@
     @Mock lateinit var dragAndDropController: DragAndDropController
     @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
     @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
+    @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
 
     private lateinit var mockitoSession: StaticMockitoSession
     private lateinit var controller: DesktopTasksController
@@ -154,13 +163,23 @@
     // 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, 200, 2240, 1400)
+    private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
+    private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
+    private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
+    private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
+    private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
+
     @Before
     fun setUp() {
         mockitoSession = mockitoSession().strictness(Strictness.LENIENT)
             .spyStatic(DesktopModeStatus::class.java).startMocking()
         whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
 
-        shellInit = Mockito.spy(ShellInit(testExecutor))
+        shellInit = spy(ShellInit(testExecutor))
         desktopModeTaskRepository = DesktopModeTaskRepository()
         desktopTasksLimiter =
                 DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer)
@@ -463,6 +482,135 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask()
+        setUpLandscapeDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+        setUpLandscapeDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+            shouldLetterbox = true)
+        setUpLandscapeDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(isResizable = false,
+            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+        setUpLandscapeDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(isResizable = false,
+            screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+        setUpLandscapeDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+        setUpPortraitDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+        setUpPortraitDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+        setUpPortraitDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(isResizable = false,
+            deviceOrientation = ORIENTATION_PORTRAIT,
+            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+        setUpPortraitDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val task = setUpFullscreenTask(isResizable = false,
+            deviceOrientation = ORIENTATION_PORTRAIT,
+            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+        setUpPortraitDisplay()
+
+        controller.moveToDesktop(task)
+        val wct = getLatestMoveToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
     fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
         val task = setUpFullscreenTask()
         val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -1224,6 +1372,185 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+                .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask()
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+                .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+                .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+            shouldLetterbox = true)
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+                .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(isResizable = false,
+            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+        setUpLandscapeDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), 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)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+                .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+                .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+        val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+        setUpPortraitDisplay()
+
+        spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), 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)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), 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)
+        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() {
+        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val spyController = spy(controller)
+        whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+        whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), 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)
+        val wct = getLatestDragToDesktopWct()
+        assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+    }
+
+    @Test
     fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
         val task = setUpFreeformTask()
         val mockSurface = mock(SurfaceControl::class.java)
@@ -1275,8 +1602,7 @@
         controller.toggleDesktopTaskSize(task)
         // Assert bounds set to stable bounds
         val wct = getLatestToggleResizeDesktopTaskWct()
-        assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds)
-                .isEqualTo(STABLE_BOUNDS)
+        assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
     }
 
     @Test
@@ -1303,8 +1629,7 @@
 
         // Assert bounds set to last bounds before maximize
         val wct = getLatestToggleResizeDesktopTaskWct()
-        assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds)
-                .isEqualTo(boundsBeforeMaximize)
+        assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
     }
 
     @Test
@@ -1345,18 +1670,67 @@
         return task
     }
 
-    private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+    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
+    ): RunningTaskInfo {
         val task = createFullscreenTask(displayId)
-        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+        val activityInfo = ActivityInfo()
+        activityInfo.screenOrientation = screenOrientation
+        with(task) {
+            topActivityInfo = activityInfo
+            isResizeable = isResizable
+            configuration.orientation = deviceOrientation
+            configuration.windowConfiguration.windowingMode = windowingMode
+
+            if (shouldLetterbox) {
+                if (deviceOrientation == ORIENTATION_LANDSCAPE &&
+                    screenOrientation == SCREEN_ORIENTATION_PORTRAIT) {
+                    // Letterbox to portrait size
+                    appCompatTaskInfo.topActivityBoundsLetterboxed = true
+                    appCompatTaskInfo.topActivityLetterboxWidth = 1200
+                    appCompatTaskInfo.topActivityLetterboxHeight = 1600
+                } else if (deviceOrientation == ORIENTATION_PORTRAIT &&
+                    screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) {
+                    // Letterbox to landscape size
+                    appCompatTaskInfo.topActivityBoundsLetterboxed = true
+                    appCompatTaskInfo.topActivityLetterboxWidth = 1600
+                    appCompatTaskInfo.topActivityLetterboxHeight = 1200
+                }
+            } else {
+                appCompatTaskInfo.topActivityBoundsLetterboxed = false
+            }
+
+            if (deviceOrientation == ORIENTATION_LANDSCAPE) {
+                configuration.windowConfiguration.appBounds = Rect(0, 0,
+                    DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
+            } else {
+                configuration.windowConfiguration.appBounds = Rect(0, 0,
+                    DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
+            }
+        }
         whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
         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)
+    }
+
+    private fun setUpPortraitDisplay() {
+        whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
+        whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+    }
+
     private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
         val task = createSplitScreenTask(displayId)
-        doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
         whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
         whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
         whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
@@ -1419,6 +1793,17 @@
         return arg.value
     }
 
+    private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
+        val arg: ArgumentCaptor<WindowContainerTransaction> =
+            ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        if (ENABLE_SHELL_TRANSITIONS) {
+            verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
+        } else {
+            verify(shellTaskOrganizer).applyTransaction(capture(arg))
+        }
+        return arg.value
+    }
+
     private fun getLatestExitDesktopWct(): WindowContainerTransaction {
         val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
         if (ENABLE_SHELL_TRANSITIONS) {
@@ -1430,6 +1815,10 @@
         return arg.value
     }
 
+    private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
+        wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+
+
     private fun verifyWCTNotExecuted() {
         if (ENABLE_SHELL_TRANSITIONS) {
             verify(transitions, never()).startTransition(anyInt(), any(), isNull())
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 38ea034..539d5b8 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
@@ -27,6 +27,7 @@
 import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
 import androidx.test.filters.SmallTest
 import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
 import com.android.dx.mockito.inline.extended.StaticMockitoSession
 import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.ShellTestCase
@@ -41,6 +42,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.any
 import org.mockito.Mockito.`when`
 import org.mockito.quality.Strictness
 
@@ -69,7 +71,7 @@
     fun setUp() {
         mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT)
                 .spyStatic(DesktopModeStatus::class.java).startMocking()
-        `when`(DesktopModeStatus.isEnabled()).thenReturn(true)
+        doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
 
         desktopTaskRepo = DesktopModeTaskRepository()
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 71eea4b..665077b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -19,11 +19,12 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 
@@ -72,8 +73,10 @@
     public void setup() {
         mMockitoSession = mockitoSession().initMocks(this)
                 .strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking();
-        when(DesktopModeStatus.isEnabled()).thenReturn(true);
+        doReturn(true).when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
         mFreeformTaskListener = new FreeformTaskListener(
+                mContext,
                 mShellInit,
                 mTaskOrganizer,
                 Optional.of(mDesktopModeTaskRepository),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 5cf9be4..240324b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -59,6 +59,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
 import com.android.window.flags.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -75,11 +76,13 @@
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
+import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -88,7 +91,9 @@
 import java.util.function.Consumer;
 
 /**
- * Tests for {@link RecentTasksController}.
+ * Tests for {@link RecentTasksController}
+ *
+ * Usage: atest WMShellUnitTests:RecentTasksControllerTest
  */
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -118,9 +123,15 @@
     private ShellInit mShellInit;
     private ShellController mShellController;
     private TestShellExecutor mMainExecutor;
+    private static StaticMockitoSession sMockitoSession;
 
     @Before
     public void setUp() {
+        sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT)
+                .mockStatic(DesktopModeStatus.class).startMocking();
+        ExtendedMockito.doReturn(true)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
         mMainExecutor = new TestShellExecutor();
         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
         mShellInit = spy(new ShellInit(mMainExecutor));
@@ -136,6 +147,11 @@
         mShellInit.init();
     }
 
+    @After
+    public void tearDown() {
+        sMockitoSession.finishMocking();
+    }
+
     @Test
     public void instantiateController_addInitCallback() {
         verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class));
@@ -275,10 +291,6 @@
 
     @Test
     public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() {
-        StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
-                DesktopModeStatus.class).startMocking();
-        when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
         ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -309,16 +321,10 @@
         // Check single entries
         assertEquals(t2, singleGroup1.getTaskInfo1());
         assertEquals(t4, singleGroup2.getTaskInfo1());
-
-        mockitoSession.finishMocking();
     }
 
     @Test
     public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() {
-        StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
-                DesktopModeStatus.class).startMocking();
-        when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
         ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -357,15 +363,12 @@
 
         // Check single entry
         assertEquals(t4, singleGroup.getTaskInfo1());
-
-        mockitoSession.finishMocking();
     }
 
     @Test
     public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() {
-        StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
-                DesktopModeStatus.class).startMocking();
-        when(DesktopModeStatus.isEnabled()).thenReturn(false);
+        ExtendedMockito.doReturn(false)
+                .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
 
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
@@ -390,16 +393,10 @@
         assertEquals(t2, recentTasks.get(1).getTaskInfo1());
         assertEquals(t3, recentTasks.get(2).getTaskInfo1());
         assertEquals(t4, recentTasks.get(3).getTaskInfo1());
-
-        mockitoSession.finishMocking();
     }
 
     @Test
     public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() {
-        StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
-                DesktopModeStatus.class).startMocking();
-        when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
         ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -435,8 +432,6 @@
         // Check single entries
         assertEquals(t2, singleGroup1.getTaskInfo1());
         assertEquals(t4, singleGroup2.getTaskInfo1());
-
-        mockitoSession.finishMocking();
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index 82e5a1c..5464508 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -56,6 +56,8 @@
     private static final Size TASK_SIZE = new Size(500, 1000);
     private static final int TASK_CORNER_RADIUS = 10;
     private static final int EDGE_RESIZE_THICKNESS = 15;
+    private static final int EDGE_RESIZE_DEBUG_THICKNESS = EDGE_RESIZE_THICKNESS
+            + (DragResizeWindowGeometry.DEBUG ? DragResizeWindowGeometry.EDGE_DEBUG_BUFFER : 0);
     private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10;
     private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
     private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
@@ -90,13 +92,14 @@
                                 EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE),
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
                                 EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
-                .addEqualityGroup(
+                .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+                                EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5),
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
-                                EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE,
-                                LARGE_CORNER_SIZE + 5),
+                                EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5))
+                .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+                                EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE),
                         new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
-                                EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE,
-                                LARGE_CORNER_SIZE + 5))
+                                EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE))
                 .testEquals();
     }
 
@@ -122,21 +125,21 @@
     private static void verifyHorizontalEdge(@NonNull Region region, @NonNull Point point) {
         assertThat(region.contains(point.x, point.y)).isTrue();
         // Horizontally along the edge is still contained.
-        assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue();
-        assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue();
+        assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue();
+        assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue();
         // Vertically along the edge is not contained.
-        assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse();
-        assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isFalse();
+        assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isFalse();
+        assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isFalse();
     }
 
     private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) {
         assertThat(region.contains(point.x, point.y)).isTrue();
         // Horizontally along the edge is not contained.
-        assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse();
-        assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse();
+        assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse();
+        assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse();
         // Vertically along the edge is contained.
-        assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue();
-        assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue();
+        assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isTrue();
+        assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isTrue();
     }
 
     /**
@@ -148,7 +151,10 @@
     public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() {
         Region region = new Region();
         GEOMETRY.union(region);
-        final int cornerRadius = LARGE_CORNER_SIZE / 2;
+        // Make sure we're choosing a point outside of any debug region buffer.
+        final int cornerRadius = DragResizeWindowGeometry.DEBUG
+                ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS)
+                : LARGE_CORNER_SIZE / 2;
 
         new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region);
     }
@@ -162,7 +168,9 @@
     public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() {
         Region region = new Region();
         GEOMETRY.union(region);
-        final int cornerRadius = FINE_CORNER_SIZE / 2;
+        final int cornerRadius = DragResizeWindowGeometry.DEBUG
+                ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS)
+                : LARGE_CORNER_SIZE / 2;
 
         new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region);
     }
diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h
index 1a5b938..31c9db7 100644
--- a/libs/hwui/ColorFilter.h
+++ b/libs/hwui/ColorFilter.h
@@ -23,17 +23,42 @@
 
 #include "GraphicsJNI.h"
 #include "SkColorFilter.h"
-#include "SkiaWrapper.h"
 
 namespace android {
 namespace uirenderer {
 
-class ColorFilter : public SkiaWrapper<SkColorFilter> {
+class ColorFilter : public VirtualLightRefBase {
 public:
     static ColorFilter* fromJava(jlong handle) { return reinterpret_cast<ColorFilter*>(handle); }
 
+    sk_sp<SkColorFilter> getInstance() {
+        if (mInstance != nullptr && shouldDiscardInstance()) {
+            mInstance = nullptr;
+        }
+
+        if (mInstance == nullptr) {
+            mInstance = createInstance();
+            if (mInstance) {
+                mInstance = mInstance->makeWithWorkingColorSpace(SkColorSpace::MakeSRGB());
+            }
+            mGenerationId++;
+        }
+        return mInstance;
+    }
+
+    virtual bool shouldDiscardInstance() const { return false; }
+
+    void discardInstance() { mInstance = nullptr; }
+
+    [[nodiscard]] int32_t getGenerationId() const { return mGenerationId; }
+
 protected:
     ColorFilter() = default;
+    virtual sk_sp<SkColorFilter> createInstance() = 0;
+
+private:
+    sk_sp<SkColorFilter> mInstance = nullptr;
+    int32_t mGenerationId = 0;
 };
 
 class BlendModeColorFilter : public ColorFilter {
diff --git a/libs/hwui/SkiaWrapper.h b/libs/hwui/SkiaWrapper.h
deleted file mode 100644
index bd0e35a..0000000
--- a/libs/hwui/SkiaWrapper.h
+++ /dev/null
@@ -1,56 +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.
- */
-
-#ifndef SKIA_WRAPPER_H_
-#define SKIA_WRAPPER_H_
-
-#include <SkRefCnt.h>
-#include <utils/RefBase.h>
-
-namespace android::uirenderer {
-
-template <typename T>
-class SkiaWrapper : public VirtualLightRefBase {
-public:
-    sk_sp<T> getInstance() {
-        if (mInstance != nullptr && shouldDiscardInstance()) {
-            mInstance = nullptr;
-        }
-
-        if (mInstance == nullptr) {
-            mInstance = createInstance();
-            mGenerationId++;
-        }
-        return mInstance;
-    }
-
-    virtual bool shouldDiscardInstance() const { return false; }
-
-    void discardInstance() { mInstance = nullptr; }
-
-    [[nodiscard]] int32_t getGenerationId() const { return mGenerationId; }
-
-protected:
-    virtual sk_sp<T> createInstance() = 0;
-
-private:
-    sk_sp<T> mInstance = nullptr;
-    int32_t mGenerationId = 0;
-};
-
-}  // namespace android::uirenderer
-
-#endif  // SKIA_WRAPPER_H_
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 6a46544..5cf5a1d 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -117,7 +117,7 @@
     return {mLocked.pointerX, mLocked.pointerY};
 }
 
-int32_t MouseCursorController::getDisplayId() const {
+ui::LogicalDisplayId MouseCursorController::getDisplayId() const {
     std::scoped_lock lock(mLock);
     return mLocked.viewport.displayId;
 }
@@ -467,10 +467,10 @@
 
     std::function<bool(nsecs_t)> func = std::bind(&MouseCursorController::doAnimations, this, _1);
     /*
-     * Using -1 for displayId here to avoid removing the callback
+     * Using ui::ADISPLAY_ID_NONE for displayId here to avoid removing the callback
      * if a TouchSpotController with the same display is removed.
      */
-    mContext.addAnimationCallback(-1, func);
+    mContext.addAnimationCallback(ui::ADISPLAY_ID_NONE, func);
 }
 
 } // namespace android
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 00dc085..dc7e8ca 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -47,7 +47,7 @@
     void move(float deltaX, float deltaY);
     void setPosition(float x, float y);
     FloatPoint getPosition() const;
-    int32_t getDisplayId() const;
+    ui::LogicalDisplayId getDisplayId() const;
     void fade(PointerControllerInterface::Transition transition);
     void unfade(PointerControllerInterface::Transition transition);
     void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources);
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 933a33e..cca1b07 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -24,7 +24,6 @@
 #include <SkColor.h>
 #include <android-base/stringprintf.h>
 #include <android-base/thread_annotations.h>
-#include <com_android_input_flags.h>
 #include <ftl/enum.h>
 
 #include <mutex>
@@ -35,14 +34,10 @@
 #define INDENT2 "    "
 #define INDENT3 "      "
 
-namespace input_flags = com::android::input::flags;
-
 namespace android {
 
 namespace {
 
-static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer();
-
 const ui::Transform kIdentityTransform;
 
 } // namespace
@@ -68,27 +63,24 @@
 
 std::shared_ptr<PointerController> PointerController::create(
         const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
-        SpriteController& spriteController, bool enabled, ControllerType type) {
+        SpriteController& spriteController, ControllerType type) {
     // using 'new' to access non-public constructor
     std::shared_ptr<PointerController> controller;
     switch (type) {
         case ControllerType::MOUSE:
             controller = std::shared_ptr<PointerController>(
-                    new MousePointerController(policy, looper, spriteController, enabled));
+                    new MousePointerController(policy, looper, spriteController));
             break;
         case ControllerType::TOUCH:
             controller = std::shared_ptr<PointerController>(
-                    new TouchPointerController(policy, looper, spriteController, enabled));
+                    new TouchPointerController(policy, looper, spriteController));
             break;
         case ControllerType::STYLUS:
             controller = std::shared_ptr<PointerController>(
-                    new StylusPointerController(policy, looper, spriteController, enabled));
+                    new StylusPointerController(policy, looper, spriteController));
             break;
-        case ControllerType::LEGACY:
         default:
-            controller = std::shared_ptr<PointerController>(
-                    new PointerController(policy, looper, spriteController, enabled));
-            break;
+            LOG_ALWAYS_FATAL("Invalid ControllerType: %d", static_cast<int>(type));
     }
 
     /*
@@ -108,10 +100,9 @@
 }
 
 PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
-                                     const sp<Looper>& looper, SpriteController& spriteController,
-                                     bool enabled)
+                                     const sp<Looper>& looper, SpriteController& spriteController)
       : PointerController(
-                policy, looper, spriteController, enabled,
+                policy, looper, spriteController,
                 [](const sp<android::gui::WindowInfosListener>& listener) {
                     auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{},
                                                       std::vector<android::gui::DisplayInfo>{});
@@ -125,11 +116,9 @@
 
 PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
                                      const sp<Looper>& looper, SpriteController& spriteController,
-                                     bool enabled,
                                      const WindowListenerRegisterConsumer& registerListener,
                                      WindowListenerUnregisterConsumer unregisterListener)
-      : mEnabled(enabled),
-        mContext(policy, looper, spriteController, *this),
+      : mContext(policy, looper, spriteController, *this),
         mCursorController(mContext),
         mDisplayInfoListener(sp<DisplayInfoListener>::make(this)),
         mUnregisterWindowInfosListener(std::move(unregisterListener)) {
@@ -142,7 +131,6 @@
 PointerController::~PointerController() {
     mDisplayInfoListener->onPointerControllerDestroyed();
     mUnregisterWindowInfosListener(mDisplayInfoListener);
-    mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0});
 }
 
 std::mutex& PointerController::getLock() const {
@@ -150,15 +138,11 @@
 }
 
 std::optional<FloatRect> PointerController::getBounds() const {
-    if (!mEnabled) return {};
-
     return mCursorController.getBounds();
 }
 
 void PointerController::move(float deltaX, float deltaY) {
-    if (!mEnabled) return;
-
-    const int32_t displayId = mCursorController.getDisplayId();
+    const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
     vec2 transformed;
     {
         std::scoped_lock lock(getLock());
@@ -169,9 +153,7 @@
 }
 
 void PointerController::setPosition(float x, float y) {
-    if (!mEnabled) return;
-
-    const int32_t displayId = mCursorController.getDisplayId();
+    const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
     vec2 transformed;
     {
         std::scoped_lock lock(getLock());
@@ -182,11 +164,7 @@
 }
 
 FloatPoint PointerController::getPosition() const {
-    if (!mEnabled) {
-        return FloatPoint{0, 0};
-    }
-
-    const int32_t displayId = mCursorController.getDisplayId();
+    const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
     const auto p = mCursorController.getPosition();
     {
         std::scoped_lock lock(getLock());
@@ -195,29 +173,21 @@
     }
 }
 
-int32_t PointerController::getDisplayId() const {
-    if (!mEnabled) return ADISPLAY_ID_NONE;
-
+ui::LogicalDisplayId PointerController::getDisplayId() const {
     return mCursorController.getDisplayId();
 }
 
 void PointerController::fade(Transition transition) {
-    if (!mEnabled) return;
-
     std::scoped_lock lock(getLock());
     mCursorController.fade(transition);
 }
 
 void PointerController::unfade(Transition transition) {
-    if (!mEnabled) return;
-
     std::scoped_lock lock(getLock());
     mCursorController.unfade(transition);
 }
 
 void PointerController::setPresentation(Presentation presentation) {
-    if (!mEnabled) return;
-
     std::scoped_lock lock(getLock());
 
     if (mLocked.presentation == presentation) {
@@ -226,33 +196,13 @@
 
     mLocked.presentation = presentation;
 
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        // When pointer choreographer is enabled, the presentation mode is only set once when the
-        // PointerController is constructed, before the display viewport is provided.
-        // TODO(b/293587049): Clean up the PointerController interface after pointer choreographer
-        // is permanently enabled. The presentation can be set in the constructor.
-        mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
-        return;
-    }
-
-    if (!mCursorController.isViewportValid()) {
-        return;
-    }
-
-    if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) {
-        // For now, we support stylus hover using the mouse cursor implementation.
-        // TODO: Add proper support for stylus hover icons.
-        mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
-
-        mCursorController.getAdditionalMouseResources();
-        clearSpotsLocked();
-    }
+    // The presentation mode is only set once when the PointerController is constructed,
+    // before the display viewport is provided.
+    mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
 }
 
 void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
-                                 BitSet32 spotIdBits, int32_t displayId) {
-    if (!mEnabled) return;
-
+                                 BitSet32 spotIdBits, ui::LogicalDisplayId displayId) {
     std::scoped_lock lock(getLock());
     std::array<PointerCoords, MAX_POINTERS> outSpotCoords{};
     const ui::Transform& transform = getTransformForDisplayLocked(displayId);
@@ -279,8 +229,6 @@
 }
 
 void PointerController::clearSpots() {
-    if (!mEnabled) return;
-
     std::scoped_lock lock(getLock());
     clearSpotsLocked();
 }
@@ -313,12 +261,6 @@
 }
 
 void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
-    struct PointerDisplayChangeArgs {
-        int32_t displayId;
-        FloatPoint cursorPosition;
-    };
-    std::optional<PointerDisplayChangeArgs> pointerDisplayChanged;
-
     { // acquire lock
         std::scoped_lock lock(getLock());
 
@@ -330,34 +272,21 @@
         mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
         if (viewport.displayId != mLocked.pointerDisplayId) {
             mLocked.pointerDisplayId = viewport.displayId;
-            pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()};
         }
     } // release lock
-
-    if (pointerDisplayChanged) {
-        // Notify the policy without holding the pointer controller lock.
-        mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId,
-                                                        pointerDisplayChanged->cursorPosition);
-    }
 }
 
 void PointerController::updatePointerIcon(PointerIconStyle iconId) {
-    if (!mEnabled) return;
-
     std::scoped_lock lock(getLock());
     mCursorController.updatePointerIcon(iconId);
 }
 
 void PointerController::setCustomPointerIcon(const SpriteIcon& icon) {
-    if (!mEnabled) return;
-
     std::scoped_lock lock(getLock());
     mCursorController.setCustomPointerIcon(icon);
 }
 
-void PointerController::setSkipScreenshot(int32_t displayId, bool skip) {
-    if (!mEnabled) return;
-
+void PointerController::setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) {
     std::scoped_lock lock(getLock());
     if (skip) {
         mLocked.displaysToSkipScreenshot.insert(displayId);
@@ -371,14 +300,14 @@
 }
 
 void PointerController::onDisplayViewportsUpdated(const std::vector<DisplayViewport>& viewports) {
-    std::unordered_set<int32_t> displayIdSet;
+    std::unordered_set<ui::LogicalDisplayId> displayIdSet;
     for (const DisplayViewport& viewport : viewports) {
         displayIdSet.insert(viewport.displayId);
     }
 
     std::scoped_lock lock(getLock());
     for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) {
-        int32_t displayId = it->first;
+        ui::LogicalDisplayId displayId = it->first;
         if (!displayIdSet.count(displayId)) {
             /*
              * Ensures that an in-progress animation won't dereference
@@ -397,7 +326,8 @@
     mLocked.mDisplayInfos = displayInfo;
 }
 
-const ui::Transform& PointerController::getTransformForDisplayLocked(int displayId) const {
+const ui::Transform& PointerController::getTransformForDisplayLocked(
+        ui::LogicalDisplayId displayId) const {
     const auto& di = mLocked.mDisplayInfos;
     auto it = std::find_if(di.begin(), di.end(), [displayId](const gui::DisplayInfo& info) {
         return info.displayId == displayId;
@@ -406,15 +336,12 @@
 }
 
 std::string PointerController::dump() {
-    if (!mEnabled) {
-        return INDENT "PointerController: DISABLED due to ongoing PointerChoreographer refactor\n";
-    }
-
     std::string dump = INDENT "PointerController:\n";
     std::scoped_lock lock(getLock());
     dump += StringPrintf(INDENT2 "Presentation: %s\n",
                          ftl::enum_string(mLocked.presentation).c_str());
-    dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId);
+    dump += StringPrintf(INDENT2 "Pointer Display ID: %s\n",
+                         mLocked.pointerDisplayId.toString().c_str());
     dump += StringPrintf(INDENT2 "Viewports:\n");
     for (const auto& info : mLocked.mDisplayInfos) {
         info.dump(dump, INDENT3);
@@ -430,8 +357,8 @@
 
 MousePointerController::MousePointerController(const sp<PointerControllerPolicyInterface>& policy,
                                                const sp<Looper>& looper,
-                                               SpriteController& spriteController, bool enabled)
-      : PointerController(policy, looper, spriteController, enabled) {
+                                               SpriteController& spriteController)
+      : PointerController(policy, looper, spriteController) {
     PointerController::setPresentation(Presentation::POINTER);
 }
 
@@ -443,8 +370,8 @@
 
 TouchPointerController::TouchPointerController(const sp<PointerControllerPolicyInterface>& policy,
                                                const sp<Looper>& looper,
-                                               SpriteController& spriteController, bool enabled)
-      : PointerController(policy, looper, spriteController, enabled) {
+                                               SpriteController& spriteController)
+      : PointerController(policy, looper, spriteController) {
     PointerController::setPresentation(Presentation::SPOT);
 }
 
@@ -456,8 +383,8 @@
 
 StylusPointerController::StylusPointerController(const sp<PointerControllerPolicyInterface>& policy,
                                                  const sp<Looper>& looper,
-                                                 SpriteController& spriteController, bool enabled)
-      : PointerController(policy, looper, spriteController, enabled) {
+                                                 SpriteController& spriteController)
+      : PointerController(policy, looper, spriteController) {
     PointerController::setPresentation(Presentation::STYLUS_HOVER);
 }
 
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index d76ca5d..70e5c24 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -47,8 +47,7 @@
 public:
     static std::shared_ptr<PointerController> create(
             const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
-            SpriteController& spriteController, bool enabled,
-            ControllerType type = ControllerType::LEGACY);
+            SpriteController& spriteController, ControllerType type);
 
     ~PointerController() override;
 
@@ -56,18 +55,18 @@
     void move(float deltaX, float deltaY) override;
     void setPosition(float x, float y) override;
     FloatPoint getPosition() const override;
-    int32_t getDisplayId() const override;
+    ui::LogicalDisplayId getDisplayId() const override;
     void fade(Transition transition) override;
     void unfade(Transition transition) override;
     void setDisplayViewport(const DisplayViewport& viewport) override;
 
     void setPresentation(Presentation presentation) override;
     void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
-                  BitSet32 spotIdBits, int32_t displayId) override;
+                  BitSet32 spotIdBits, ui::LogicalDisplayId displayId) override;
     void clearSpots() override;
     void updatePointerIcon(PointerIconStyle iconId) override;
     void setCustomPointerIcon(const SpriteIcon& icon) override;
-    void setSkipScreenshot(int32_t displayId, bool skip) override;
+    void setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) override;
 
     virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
     void doInactivityTimeout();
@@ -87,12 +86,12 @@
 
     // Constructor used to test WindowInfosListener registration.
     PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
-                      SpriteController& spriteController, bool enabled,
+                      SpriteController& spriteController,
                       const WindowListenerRegisterConsumer& registerListener,
                       WindowListenerUnregisterConsumer unregisterListener);
 
     PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
-                      SpriteController& spriteController, bool enabled);
+                      SpriteController& spriteController);
 
 private:
     friend PointerControllerContext::LooperCallback;
@@ -104,19 +103,17 @@
     // we use the DisplayInfoListener's lock in PointerController.
     std::mutex& getLock() const;
 
-    const bool mEnabled;
-
     PointerControllerContext mContext;
 
     MouseCursorController mCursorController;
 
     struct Locked {
         Presentation presentation;
-        int32_t pointerDisplayId = ADISPLAY_ID_NONE;
+        ui::LogicalDisplayId pointerDisplayId = ui::ADISPLAY_ID_NONE;
 
         std::vector<gui::DisplayInfo> mDisplayInfos;
-        std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers;
-        std::unordered_set<int32_t /* displayId */> displaysToSkipScreenshot;
+        std::unordered_map<ui::LogicalDisplayId, TouchSpotController> spotControllers;
+        std::unordered_set<ui::LogicalDisplayId> displaysToSkipScreenshot;
     } mLocked GUARDED_BY(getLock());
 
     class DisplayInfoListener : public gui::WindowInfosListener {
@@ -135,7 +132,8 @@
     sp<DisplayInfoListener> mDisplayInfoListener;
     const WindowListenerUnregisterConsumer mUnregisterWindowInfosListener;
 
-    const ui::Transform& getTransformForDisplayLocked(int displayId) const REQUIRES(getLock());
+    const ui::Transform& getTransformForDisplayLocked(ui::LogicalDisplayId displayId) const
+            REQUIRES(getLock());
 
     void clearSpotsLocked() REQUIRES(getLock());
 };
@@ -144,15 +142,14 @@
 public:
     /** A version of PointerController that controls one mouse pointer. */
     MousePointerController(const sp<PointerControllerPolicyInterface>& policy,
-                           const sp<Looper>& looper, SpriteController& spriteController,
-                           bool enabled);
+                           const sp<Looper>& looper, SpriteController& spriteController);
 
     ~MousePointerController() override;
 
     void setPresentation(Presentation) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
-    void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override {
+    void setSpots(const PointerCoords*, const uint32_t*, BitSet32, ui::LogicalDisplayId) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
     void clearSpots() override {
@@ -164,8 +161,7 @@
 public:
     /** A version of PointerController that controls touch spots. */
     TouchPointerController(const sp<PointerControllerPolicyInterface>& policy,
-                           const sp<Looper>& looper, SpriteController& spriteController,
-                           bool enabled);
+                           const sp<Looper>& looper, SpriteController& spriteController);
 
     ~TouchPointerController() override;
 
@@ -181,7 +177,7 @@
     FloatPoint getPosition() const override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
-    int32_t getDisplayId() const override {
+    ui::LogicalDisplayId getDisplayId() const override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
     void fade(Transition) override {
@@ -210,15 +206,14 @@
 public:
     /** A version of PointerController that controls one stylus pointer. */
     StylusPointerController(const sp<PointerControllerPolicyInterface>& policy,
-                            const sp<Looper>& looper, SpriteController& spriteController,
-                            bool enabled);
+                            const sp<Looper>& looper, SpriteController& spriteController);
 
     ~StylusPointerController() override;
 
     void setPresentation(Presentation) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
-    void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override {
+    void setSpots(const PointerCoords*, const uint32_t*, BitSet32, ui::LogicalDisplayId) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
     void clearSpots() override {
diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp
index 15c3517..747eb8e 100644
--- a/libs/input/PointerControllerContext.cpp
+++ b/libs/input/PointerControllerContext.cpp
@@ -138,12 +138,12 @@
     return 1; // keep the callback
 }
 
-void PointerControllerContext::addAnimationCallback(int32_t displayId,
+void PointerControllerContext::addAnimationCallback(ui::LogicalDisplayId displayId,
                                                     std::function<bool(nsecs_t)> callback) {
     mAnimator.addCallback(displayId, callback);
 }
 
-void PointerControllerContext::removeAnimationCallback(int32_t displayId) {
+void PointerControllerContext::removeAnimationCallback(ui::LogicalDisplayId displayId) {
     mAnimator.removeCallback(displayId);
 }
 
@@ -161,14 +161,14 @@
     }
 }
 
-void PointerControllerContext::PointerAnimator::addCallback(int32_t displayId,
+void PointerControllerContext::PointerAnimator::addCallback(ui::LogicalDisplayId displayId,
                                                             std::function<bool(nsecs_t)> callback) {
     std::scoped_lock lock(mLock);
     mLocked.callbacks[displayId] = callback;
     startAnimationLocked();
 }
 
-void PointerControllerContext::PointerAnimator::removeCallback(int32_t displayId) {
+void PointerControllerContext::PointerAnimator::removeCallback(ui::LogicalDisplayId displayId) {
     std::scoped_lock lock(mLock);
     auto it = mLocked.callbacks.find(displayId);
     if (it == mLocked.callbacks.end()) {
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index 98c3988..d422148 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -72,16 +72,16 @@
     virtual ~PointerControllerPolicyInterface() {}
 
 public:
-    virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) = 0;
-    virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) = 0;
+    virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) = 0;
+    virtual void loadPointerResources(PointerResources* outResources,
+                                      ui::LogicalDisplayId displayId) = 0;
     virtual void loadAdditionalMouseResources(
             std::map<PointerIconStyle, SpriteIcon>* outResources,
             std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
-            int32_t displayId) = 0;
+            ui::LogicalDisplayId displayId) = 0;
     virtual PointerIconStyle getDefaultPointerIconId() = 0;
     virtual PointerIconStyle getDefaultStylusIconId() = 0;
     virtual PointerIconStyle getCustomPointerIconId() = 0;
-    virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0;
 };
 
 /*
@@ -103,7 +103,7 @@
 
     nsecs_t getAnimationTime();
 
-    void clearSpotsByDisplay(int32_t displayId);
+    void clearSpotsByDisplay(ui::LogicalDisplayId displayId);
 
     void setHandlerController(std::shared_ptr<PointerController> controller);
     void setCallbackController(std::shared_ptr<PointerController> controller);
@@ -113,8 +113,9 @@
 
     void handleDisplayEvents();
 
-    void addAnimationCallback(int32_t displayId, std::function<bool(nsecs_t)> callback);
-    void removeAnimationCallback(int32_t displayId);
+    void addAnimationCallback(ui::LogicalDisplayId displayId,
+                              std::function<bool(nsecs_t)> callback);
+    void removeAnimationCallback(ui::LogicalDisplayId displayId);
 
     class MessageHandler : public virtual android::MessageHandler {
     public:
@@ -137,8 +138,8 @@
     public:
         PointerAnimator(PointerControllerContext& context);
 
-        void addCallback(int32_t displayId, std::function<bool(nsecs_t)> callback);
-        void removeCallback(int32_t displayId);
+        void addCallback(ui::LogicalDisplayId displayId, std::function<bool(nsecs_t)> callback);
+        void removeCallback(ui::LogicalDisplayId displayId);
         void handleVsyncEvents();
         nsecs_t getAnimationTimeLocked();
 
@@ -149,7 +150,7 @@
             bool animationPending{false};
             nsecs_t animationTime{systemTime(SYSTEM_TIME_MONOTONIC)};
 
-            std::unordered_map<int32_t, std::function<bool(nsecs_t)>> callbacks;
+            std::unordered_map<ui::LogicalDisplayId, std::function<bool(nsecs_t)>> callbacks;
         } mLocked GUARDED_BY(mLock);
 
         DisplayEventReceiver mDisplayEventReceiver;
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index 0baa929..af49939 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -19,9 +19,9 @@
 
 #include "SpriteController.h"
 
-#include <log/log.h>
-#include <utils/String8.h>
+#include <android-base/logging.h>
 #include <gui/Surface.h>
+#include <utils/String8.h>
 
 namespace android {
 
@@ -340,13 +340,14 @@
     }
 }
 
-sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height, int32_t displayId,
+sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height,
+                                                   ui::LogicalDisplayId displayId,
                                                    bool hideOnMirrored) {
     ensureSurfaceComposerClient();
 
     const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId);
     if (parent == nullptr) {
-        ALOGE("Failed to get the parent surface for pointers on display %d", displayId);
+        LOG(ERROR) << "Failed to get the parent surface for pointers on display " << displayId;
     }
 
     int32_t createFlags = ISurfaceComposerClient::eHidden | ISurfaceComposerClient::eCursorWindow;
@@ -475,7 +476,7 @@
     }
 }
 
-void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
+void SpriteController::SpriteImpl::setDisplayId(ui::LogicalDisplayId displayId) {
     AutoMutex _l(mController.mLock);
 
     if (mLocked.state.displayId != displayId) {
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 4e4ba65..070c90c 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -95,7 +95,7 @@
     virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;
 
     /* Sets the id of the display where the sprite should be shown. */
-    virtual void setDisplayId(int32_t displayId) = 0;
+    virtual void setDisplayId(ui::LogicalDisplayId displayId) = 0;
 
     /* Sets the flag to hide sprite on mirrored displays.
      * This will add ISurfaceComposerClient::eSkipScreenshot flag to the sprite. */
@@ -115,7 +115,7 @@
  */
 class SpriteController {
 public:
-    using ParentSurfaceProvider = std::function<sp<SurfaceControl>(int /*displayId*/)>;
+    using ParentSurfaceProvider = std::function<sp<SurfaceControl>(ui::LogicalDisplayId)>;
     SpriteController(const sp<Looper>& looper, int32_t overlayLayer, ParentSurfaceProvider parent);
     SpriteController(const SpriteController&) = delete;
     SpriteController& operator=(const SpriteController&) = delete;
@@ -165,29 +165,23 @@
      * on the sprites for a long time.
      * Note that the SpriteIcon holds a reference to a shared (and immutable) bitmap. */
     struct SpriteState {
-        inline SpriteState() :
-                dirty(0), visible(false),
-                positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT),
-                surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
-        }
-
-        uint32_t dirty;
+        uint32_t dirty{0};
 
         SpriteIcon icon;
-        bool visible;
-        float positionX;
-        float positionY;
-        int32_t layer;
-        float alpha;
+        bool visible{false};
+        float positionX{0};
+        float positionY{0};
+        int32_t layer{0};
+        float alpha{1.0f};
         SpriteTransformationMatrix transformationMatrix;
-        int32_t displayId;
+        ui::LogicalDisplayId displayId{ui::ADISPLAY_ID_DEFAULT};
 
         sp<SurfaceControl> surfaceControl;
-        int32_t surfaceWidth;
-        int32_t surfaceHeight;
-        bool surfaceDrawn;
-        bool surfaceVisible;
-        bool skipScreenshot;
+        int32_t surfaceWidth{0};
+        int32_t surfaceHeight{0};
+        bool surfaceDrawn{false};
+        bool surfaceVisible{false};
+        bool skipScreenshot{false};
 
         inline bool wantSurfaceVisible() const {
             return visible && alpha > 0.0f && icon.isValid();
@@ -214,7 +208,7 @@
         virtual void setLayer(int32_t layer);
         virtual void setAlpha(float alpha);
         virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
-        virtual void setDisplayId(int32_t displayId);
+        virtual void setDisplayId(ui::LogicalDisplayId displayId);
         virtual void setSkipScreenshot(bool skip);
 
         inline const SpriteState& getStateLocked() const {
@@ -279,7 +273,7 @@
     void doDisposeSurfaces();
 
     void ensureSurfaceComposerClient();
-    sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId,
+    sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, ui::LogicalDisplayId displayId,
                                      bool hideOnMirrored);
 };
 
diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp
index 530d541..7462481 100644
--- a/libs/input/TouchSpotController.cpp
+++ b/libs/input/TouchSpotController.cpp
@@ -40,7 +40,7 @@
 // --- Spot ---
 
 void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float newX, float newY,
-                                             int32_t displayId, bool skipScreenshot) {
+                                             ui::LogicalDisplayId displayId, bool skipScreenshot) {
     sprite->setLayer(Sprite::BASE_LAYER_SPOT + id);
     sprite->setAlpha(alpha);
     sprite->setTransformationMatrix(SpriteTransformationMatrix(scale, 0.0f, 0.0f, scale));
@@ -69,7 +69,8 @@
 
 // --- TouchSpotController ---
 
-TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context)
+TouchSpotController::TouchSpotController(ui::LogicalDisplayId displayId,
+                                         PointerControllerContext& context)
       : mDisplayId(displayId), mContext(context) {
     mContext.getPolicy()->loadPointerResources(&mResources, mDisplayId);
 }
@@ -94,7 +95,7 @@
         const PointerCoords& c = spotCoords[spotIdToIndex[id]];
         ALOGD(" spot %d: position=(%0.3f, %0.3f), pressure=%0.3f, displayId=%" PRId32 ".", id,
               c.getAxisValue(AMOTION_EVENT_AXIS_X), c.getAxisValue(AMOTION_EVENT_AXIS_Y),
-              c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId);
+              c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId.id);
     }
 #endif
 
@@ -274,7 +275,7 @@
     out += prefix;
     out += "SpotController:\n";
     out += prefix;
-    StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId);
+    StringAppendF(&out, INDENT "DisplayId: %s\n", mDisplayId.toString().c_str());
     std::scoped_lock lock(mLock);
     out += prefix;
     StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating));
diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h
index 608653c..ac37fa4 100644
--- a/libs/input/TouchSpotController.h
+++ b/libs/input/TouchSpotController.h
@@ -29,7 +29,7 @@
  */
 class TouchSpotController {
 public:
-    TouchSpotController(int32_t displayId, PointerControllerContext& context);
+    TouchSpotController(ui::LogicalDisplayId displayId, PointerControllerContext& context);
     ~TouchSpotController();
     void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
                   BitSet32 spotIdBits, bool skipScreenshot);
@@ -59,7 +59,7 @@
                 y(0.0f),
                 mLastIcon(nullptr) {}
 
-        void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId,
+        void updateSprite(const SpriteIcon* icon, float x, float y, ui::LogicalDisplayId displayId,
                           bool skipScreenshot);
         void dump(std::string& out, const char* prefix = "") const;
 
@@ -67,7 +67,7 @@
         const SpriteIcon* mLastIcon;
     };
 
-    int32_t mDisplayId;
+    ui::LogicalDisplayId mDisplayId;
 
     mutable std::mutex mLock;
 
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index fcf226c..7a13380 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-#include <com_android_input_flags.h>
 #include <flag_macros.h>
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
@@ -30,8 +29,6 @@
 
 namespace android {
 
-namespace input_flags = com::android::input::flags;
-
 enum TestCursorType {
     CURSOR_TYPE_DEFAULT = 0,
     CURSOR_TYPE_HOVER,
@@ -55,20 +52,19 @@
 
 class MockPointerControllerPolicyInterface : public PointerControllerPolicyInterface {
 public:
-    virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) override;
-    virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) override;
+    virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) override;
+    virtual void loadPointerResources(PointerResources* outResources,
+                                      ui::LogicalDisplayId displayId) override;
     virtual void loadAdditionalMouseResources(
             std::map<PointerIconStyle, SpriteIcon>* outResources,
             std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
-            int32_t displayId) override;
+            ui::LogicalDisplayId displayId) override;
     virtual PointerIconStyle getDefaultPointerIconId() override;
     virtual PointerIconStyle getDefaultStylusIconId() override;
     virtual PointerIconStyle getCustomPointerIconId() override;
-    virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
 
     bool allResourcesAreLoaded();
     bool noResourcesAreLoaded();
-    std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; }
 
 private:
     void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType);
@@ -76,16 +72,15 @@
     bool pointerIconLoaded{false};
     bool pointerResourcesLoaded{false};
     bool additionalMouseResourcesLoaded{false};
-    std::optional<int32_t /*displayId*/> latestPointerDisplayId;
 };
 
-void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) {
+void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId) {
     loadPointerIconForType(icon, CURSOR_TYPE_DEFAULT);
     pointerIconLoaded = true;
 }
 
 void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources* outResources,
-        int32_t) {
+                                                                ui::LogicalDisplayId) {
     loadPointerIconForType(&outResources->spotHover, CURSOR_TYPE_HOVER);
     loadPointerIconForType(&outResources->spotTouch, CURSOR_TYPE_TOUCH);
     loadPointerIconForType(&outResources->spotAnchor, CURSOR_TYPE_ANCHOR);
@@ -94,7 +89,7 @@
 
 void MockPointerControllerPolicyInterface::loadAdditionalMouseResources(
         std::map<PointerIconStyle, SpriteIcon>* outResources,
-        std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t) {
+        std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, ui::LogicalDisplayId) {
     SpriteIcon icon;
     PointerAnimation anim;
 
@@ -146,12 +141,6 @@
     icon->hotSpotY = hotSpot.second;
 }
 
-void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId,
-                                                                     const FloatPoint& /*position*/
-) {
-    latestPointerDisplayId = displayId;
-}
-
 class TestPointerController : public PointerController {
 public:
     TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener,
@@ -159,7 +148,6 @@
                           SpriteController& spriteController)
           : PointerController(
                     policy, looper, spriteController,
-                    /*enabled=*/true,
                     [&registeredListener](const sp<android::gui::WindowInfosListener>& listener)
                             -> std::vector<gui::DisplayInfo> {
                         // Register listener
@@ -178,7 +166,7 @@
     PointerControllerTest();
     ~PointerControllerTest();
 
-    void ensureDisplayViewportIsSet(int32_t displayId = ADISPLAY_ID_DEFAULT);
+    void ensureDisplayViewportIsSet(ui::LogicalDisplayId displayId = ui::ADISPLAY_ID_DEFAULT);
 
     sp<MockSprite> mPointerSprite;
     sp<MockPointerControllerPolicyInterface> mPolicy;
@@ -217,7 +205,7 @@
     mThread.join();
 }
 
-void PointerControllerTest::ensureDisplayViewportIsSet(int32_t displayId) {
+void PointerControllerTest::ensureDisplayViewportIsSet(ui::LogicalDisplayId displayId) {
     DisplayViewport viewport;
     viewport.displayId = displayId;
     viewport.logicalRight = 1600;
@@ -267,8 +255,7 @@
     mPointerController->reloadPointerResources();
 }
 
-TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources,
-                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) {
+TEST_F(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources) {
     // Setting the presentation mode before a display viewport is set will not load any resources.
     mPointerController->setPresentation(PointerController::Presentation::POINTER);
     ASSERT_TRUE(mPolicy->noResourcesAreLoaded());
@@ -278,26 +265,7 @@
     ASSERT_TRUE(mPolicy->allResourcesAreLoaded());
 }
 
-TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIcon,
-                  REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(input_flags,
-                                                       enable_pointer_choreographer))) {
-    ensureDisplayViewportIsSet();
-    mPointerController->setPresentation(PointerController::Presentation::POINTER);
-    mPointerController->unfade(PointerController::Transition::IMMEDIATE);
-
-    int32_t type = CURSOR_TYPE_ADDITIONAL;
-    std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type);
-    EXPECT_CALL(*mPointerSprite, setVisible(true));
-    EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
-    EXPECT_CALL(*mPointerSprite,
-                setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)),
-                              Field(&SpriteIcon::hotSpotX, hotspot.first),
-                              Field(&SpriteIcon::hotSpotY, hotspot.second))));
-    mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type));
-}
-
-TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIconWithChoreographer,
-                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) {
+TEST_F(PointerControllerTest, updatePointerIconWithChoreographer) {
     // When PointerChoreographer is enabled, the presentation mode is set before the viewport.
     mPointerController->setPresentation(PointerController::Presentation::POINTER);
     ensureDisplayViewportIsSet();
@@ -348,30 +316,6 @@
     ensureDisplayViewportIsSet();
 }
 
-TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) {
-    EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId())
-            << "A pointer display change does not occur when PointerController is created.";
-
-    ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT);
-
-    const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId();
-    ASSERT_TRUE(lastReportedPointerDisplayId)
-            << "The policy is notified of a pointer display change when the viewport is first set.";
-    EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId)
-            << "Incorrect pointer display notified.";
-
-    ensureDisplayViewportIsSet(42);
-
-    EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId())
-            << "The policy is notified when the pointer display changes.";
-
-    // Release the PointerController.
-    mPointerController = nullptr;
-
-    EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId())
-            << "The pointer display changes to invalid when PointerController is destroyed.";
-}
-
 TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) {
     ensureDisplayViewportIsSet();
 
@@ -391,23 +335,23 @@
 
     // Update spots to sync state with sprite
     mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
-                                 ADISPLAY_ID_DEFAULT);
+                                 ui::ADISPLAY_ID_DEFAULT);
     testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
 
     // Marking the display to skip screenshot should update sprite as well
-    mPointerController->setSkipScreenshot(ADISPLAY_ID_DEFAULT, true);
+    mPointerController->setSkipScreenshot(ui::ADISPLAY_ID_DEFAULT, true);
     EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(true));
 
     // Update spots to sync state with sprite
     mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
-                                 ADISPLAY_ID_DEFAULT);
+                                 ui::ADISPLAY_ID_DEFAULT);
     testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
 
     // Reset flag and verify again
-    mPointerController->setSkipScreenshot(ADISPLAY_ID_DEFAULT, false);
+    mPointerController->setSkipScreenshot(ui::ADISPLAY_ID_DEFAULT, false);
     EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
     mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
-                                 ADISPLAY_ID_DEFAULT);
+                                 ui::ADISPLAY_ID_DEFAULT);
     testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
 }
 
diff --git a/libs/input/tests/mocks/MockSprite.h b/libs/input/tests/mocks/MockSprite.h
index 0867221..21628fb 100644
--- a/libs/input/tests/mocks/MockSprite.h
+++ b/libs/input/tests/mocks/MockSprite.h
@@ -33,7 +33,7 @@
     MOCK_METHOD(void, setLayer, (int32_t), (override));
     MOCK_METHOD(void, setAlpha, (float), (override));
     MOCK_METHOD(void, setTransformationMatrix, (const SpriteTransformationMatrix&), (override));
-    MOCK_METHOD(void, setDisplayId, (int32_t), (override));
+    MOCK_METHOD(void, setDisplayId, (ui::LogicalDisplayId), (override));
     MOCK_METHOD(void, setSkipScreenshot, (bool), (override));
 };
 
diff --git a/libs/input/tests/mocks/MockSpriteController.h b/libs/input/tests/mocks/MockSpriteController.h
index 62f1d65..9ef6b7c 100644
--- a/libs/input/tests/mocks/MockSpriteController.h
+++ b/libs/input/tests/mocks/MockSpriteController.h
@@ -27,7 +27,7 @@
 
 public:
     MockSpriteController(sp<Looper> looper)
-          : SpriteController(looper, 0, [](int) { return nullptr; }) {}
+          : SpriteController(looper, 0, [](ui::LogicalDisplayId) { return nullptr; }) {}
     ~MockSpriteController() {}
 
     MOCK_METHOD(sp<Sprite>, createSprite, (), (override));
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index b2838c8..7ddf11e 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -50,6 +50,7 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.media.flags.Flags;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -984,37 +985,7 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
     public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
-        mImpl.transfer(
-                controller.getRoutingSessionInfo(),
-                route,
-                Process.myUserHandle(),
-                mContext.getPackageName());
-    }
-
-    /**
-     * Transfers the media of a routing controller to the given route.
-     *
-     * <p>This will be no-op for non-system media routers.
-     *
-     * @param controller a routing controller controlling media routing.
-     * @param route the route you want to transfer the media to.
-     * @param transferInitiatorUserHandle the user handle of the app that initiated the transfer
-     *     request.
-     * @param transferInitiatorPackageName the package name of the app that initiated the transfer.
-     *     This value is used with the user handle to populate {@link
-     *     RoutingController#wasTransferInitiatedBySelf()}.
-     * @hide
-     */
-    public void transfer(
-            @NonNull RoutingController controller,
-            @NonNull MediaRoute2Info route,
-            @NonNull UserHandle transferInitiatorUserHandle,
-            @NonNull String transferInitiatorPackageName) {
-        mImpl.transfer(
-                controller.getRoutingSessionInfo(),
-                route,
-                transferInitiatorUserHandle,
-                transferInitiatorPackageName);
+        mImpl.transfer(controller.getRoutingSessionInfo(), route);
     }
 
     void requestCreateController(
@@ -1913,13 +1884,7 @@
          */
         @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
         public boolean wasTransferInitiatedBySelf() {
-            RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
-
-            UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
-            String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
-
-            return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
-                    && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+            return mImpl.wasTransferredBySelf(getRoutingSessionInfo());
         }
 
         /**
@@ -2082,12 +2047,28 @@
             Objects.requireNonNull(route, "route must not be null");
             synchronized (mControllerLock) {
                 if (isReleased()) {
-                    Log.w(TAG, "transferToRoute: Called on released controller. Ignoring.");
+                    Log.w(
+                            TAG,
+                            "tryTransferWithinProvider: Called on released controller. Ignoring.");
                     return true;
                 }
 
-                if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) {
-                    Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route);
+                // If this call is trying to transfer to a selected system route, we let them
+                // through as a provider driven transfer in order to update the transfer reason and
+                // initiator data.
+                boolean isSystemRouteReselection =
+                        Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+                                && mSessionInfo.isSystemSession()
+                                && route.isSystemRoute()
+                                && mSessionInfo.getSelectedRoutes().contains(route.getId());
+                if (!isSystemRouteReselection
+                        && !mSessionInfo.getTransferableRoutes().contains(route.getId())) {
+                    Log.i(
+                            TAG,
+                            "Transferring to a non-transferable route="
+                                    + route
+                                    + " session= "
+                                    + mSessionInfo.getId());
                     return false;
                 }
             }
@@ -2498,11 +2479,7 @@
 
         void stop();
 
-        void transfer(
-                @NonNull RoutingSessionInfo sessionInfo,
-                @NonNull MediaRoute2Info route,
-                @NonNull UserHandle transferInitiatorUserHandle,
-                @NonNull String transferInitiatorPackageName);
+        void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route);
 
         List<RoutingController> getControllers();
 
@@ -2523,6 +2500,11 @@
                 boolean shouldNotifyStop,
                 RoutingController controller);
 
+        /**
+         * Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app
+         * associated with this router.
+         */
+        boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo);
     }
 
     /**
@@ -2723,7 +2705,7 @@
 
             List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
             RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
-            transfer(targetSession, route, mClientUser, mContext.getPackageName());
+            transfer(targetSession, route);
         }
 
         @Override
@@ -2746,24 +2728,15 @@
          *
          * @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer.
          * @param route The {@link MediaRoute2Info route} to transfer to.
-         * @param transferInitiatorUserHandle The user handle of the app that initiated the
-         *     transfer.
-         * @param transferInitiatorPackageName The package name if of the app that initiated the
-         *     transfer.
          * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String)
          * @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)
          */
         @Override
         @SuppressWarnings("AndroidFrameworkRequiresPermission")
         public void transfer(
-                @NonNull RoutingSessionInfo sessionInfo,
-                @NonNull MediaRoute2Info route,
-                @NonNull UserHandle transferInitiatorUserHandle,
-                @NonNull String transferInitiatorPackageName) {
+                @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
             Objects.requireNonNull(route, "route must not be null");
-            Objects.requireNonNull(transferInitiatorUserHandle);
-            Objects.requireNonNull(transferInitiatorPackageName);
 
             Log.v(
                     TAG,
@@ -2780,15 +2753,19 @@
                 return;
             }
 
-            if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
-                transferToRoute(
-                        sessionInfo,
-                        route,
-                        transferInitiatorUserHandle,
-                        transferInitiatorPackageName);
+            // If this call is trying to transfer to a selected system route, we let them
+            // through as a provider driven transfer in order to update the transfer reason and
+            // initiator data.
+            boolean isSystemRouteReselection =
+                    Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+                            && sessionInfo.isSystemSession()
+                            && route.isSystemRoute()
+                            && sessionInfo.getSelectedRoutes().contains(route.getId());
+            if (sessionInfo.getTransferableRoutes().contains(route.getId())
+                    || isSystemRouteReselection) {
+                transferToRoute(sessionInfo, route, mClientUser, mClientPackageName);
             } else {
-                requestCreateSession(sessionInfo, route, transferInitiatorUserHandle,
-                        transferInitiatorPackageName);
+                requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName);
             }
         }
 
@@ -3043,6 +3020,14 @@
             releaseSession(controller.getRoutingSessionInfo());
         }
 
+        @Override
+        public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
+            UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+            String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+            return Objects.equals(mClientUser, transferInitiatorUserHandle)
+                    && Objects.equals(mClientPackageName, transferInitiatorPackageName);
+        }
+
         /**
          * Retrieves the system session info for the given package.
          *
@@ -3619,10 +3604,7 @@
          */
         @Override
         public void transfer(
-                @NonNull RoutingSessionInfo sessionInfo,
-                @NonNull MediaRoute2Info route,
-                @NonNull UserHandle transferInitiatorUserHandle,
-                @NonNull String transferInitiatorPackageName) {
+                @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
             // Do nothing.
         }
 
@@ -3741,6 +3723,14 @@
             }
         }
 
+        @Override
+        public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
+            UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+            String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+            return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
+                    && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+        }
+
         @GuardedBy("mLock")
         private void registerRouterStubIfNeededLocked() throws RemoteException {
             if (mStub == null) {
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index 58f56b8..78a5357 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -16,6 +16,7 @@
 
 #define LOG_TAG "PerformanceHintNativeTest"
 
+#include <aidl/android/hardware/power/ChannelConfig.h>
 #include <aidl/android/hardware/power/SessionConfig.h>
 #include <aidl/android/hardware/power/SessionTag.h>
 #include <aidl/android/hardware/power/WorkDuration.h>
@@ -54,6 +55,9 @@
     MOCK_METHOD(ScopedAStatus, getHintSessionThreadIds,
                 (const std::shared_ptr<IHintSession>& hintSession, ::std::vector<int32_t>* tids),
                 (override));
+    MOCK_METHOD(ScopedAStatus, getSessionChannel,
+                (const ::ndk::SpAIBinder& in_token, hal::ChannelConfig* _aidl_return), (override));
+    MOCK_METHOD(ScopedAStatus, closeSessionChannel, (), (override));
     MOCK_METHOD(SpAIBinder, asBinder, (), (override));
     MOCK_METHOD(bool, isRemote, (), (override));
 };
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index de9eada1..2fe2ce3 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -1212,16 +1212,16 @@
      *
      * @param service The ComponentName of the service
      * @param status  true to enable, false to disable
+     * @param userId the user handle of the user whose information is being requested.
      * @return set service for the category and true if service is already set return false.
      *
      * @hide
      */
-    public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status) {
+    public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status,
+                                                     int userId) {
         if (service == null) {
             throw new NullPointerException("activity or service or category is null");
         }
-        int userId = mContext.getUser().getIdentifier();
-
         try {
             return sService.setServiceEnabledForCategoryOther(userId, service, status);
         } catch (RemoteException e) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 50ebdd5..0da32bd 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -18,6 +18,7 @@
 
 import android.app.PendingIntent
 import android.app.assist.AssistStructure
+import android.content.ComponentName
 import android.content.Context
 import android.credentials.CredentialManager
 import android.credentials.GetCredentialRequest
@@ -34,6 +35,10 @@
 import android.os.OutcomeReceiver
 import android.os.ResultReceiver
 import android.service.autofill.AutofillService
+import com.android.credentialmanager.model.get.ProviderInfo
+import androidx.core.graphics.drawable.toBitmap
+import com.android.credentialmanager.model.get.ActionEntryInfo
+import com.android.credentialmanager.model.EntryInfo
 import android.service.autofill.Dataset
 import android.service.autofill.Field
 import android.service.autofill.FillCallback
@@ -63,7 +68,8 @@
 import com.android.credentialmanager.getflow.toProviderDisplayInfo
 import com.android.credentialmanager.ktx.credentialEntry
 import com.android.credentialmanager.model.CredentialType
-import com.android.credentialmanager.model.get.CredentialEntryInfo
+import java.util.ArrayList
+import java.util.Objects
 import java.util.concurrent.Executors
 import org.json.JSONException
 import org.json.JSONObject
@@ -121,8 +127,11 @@
 
         val responseClientState = Bundle()
         responseClientState.putBoolean(WEBVIEW_REQUESTED_CREDENTIAL_KEY, false)
-        val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId,
-                requestId, resultReceiver, responseClientState)
+        val uniqueAutofillIdsForRequest: MutableSet<AutofillId> = mutableSetOf()
+        val getCredRequest: GetCredentialRequest? = getCredManRequest(
+            structure, sessionId,
+            requestId, resultReceiver, responseClientState, uniqueAutofillIdsForRequest
+        )
         // TODO(b/324635774): Use callback for validating. If the request is coming
         // directly from the view, there should be a corresponding callback, otherwise
         // we should fail fast,
@@ -132,14 +141,17 @@
             return
         }
         val credentialManager: CredentialManager =
-                getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
+            getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
 
         val outcome = object : OutcomeReceiver<GetCandidateCredentialsResponse,
                 GetCandidateCredentialsException> {
             override fun onResult(result: GetCandidateCredentialsResponse) {
                 Log.i(TAG, "getCandidateCredentials onResult")
-                val fillResponse = convertToFillResponse(result, request,
-                    responseClientState, GetFlowUtils.extractTypePriorityMap(getCredRequest))
+                val fillResponse = convertToFillResponse(
+                    result, request,
+                    responseClientState, GetFlowUtils.extractTypePriorityMap(getCredRequest),
+                    uniqueAutofillIdsForRequest
+                )
                 if (fillResponse != null) {
                     callback.onSuccess(fillResponse)
                 } else {
@@ -195,58 +207,131 @@
 
     private fun convertToFillResponse(
             getCredResponse: GetCandidateCredentialsResponse,
-            filLRequest: FillRequest,
+            fillRequest: FillRequest,
             responseClientState: Bundle,
             typePriorityMap: Map<String, Int>,
+            uniqueAutofillIdsForRequest: MutableSet<AutofillId>
     ): FillResponse? {
         val candidateProviders = getCredResponse.candidateProviderDataList
         if (candidateProviders.isEmpty()) {
             return null
         }
-
+        val primaryProviderComponentName = getCredResponse.primaryProviderComponentName
         val entryIconMap: Map<String, Icon> = getEntryToIconMap(candidateProviders)
         val autofillIdToProvidersMap: Map<AutofillId, ArrayList<GetCredentialProviderData>> =
-                mapAutofillIdToProviders(candidateProviders)
+            mapAutofillIdToProviders(
+                uniqueAutofillIdsForRequest,
+                candidateProviders,
+                primaryProviderComponentName
+            )
         val fillResponseBuilder = FillResponse.Builder()
         fillResponseBuilder.setFlags(FillResponse.FLAG_CREDENTIAL_MANAGER_RESPONSE)
-        var validFillResponse = false
         autofillIdToProvidersMap.forEach { (autofillId, providers) ->
-            validFillResponse = processProvidersForAutofillId(
-                    filLRequest, autofillId, providers, entryIconMap, fillResponseBuilder,
-                    getCredResponse.intent, typePriorityMap)
-                    .or(validFillResponse)
+            var credentialDatasetAdded = addCredentialDatasetsForAutofillId(fillRequest,
+                autofillId, providers, entryIconMap, fillResponseBuilder, typePriorityMap)
+            if (!credentialDatasetAdded && primaryProviderComponentName != null) {
+                val providerList = GetFlowUtils.toProviderList(
+                    providers,
+                    this@CredentialAutofillService
+                )
+                val primaryProviderInfo =
+                    providerList.find { provider -> primaryProviderComponentName
+                        .flattenToString().equals(provider.id) }
+                if (primaryProviderInfo != null) {
+                    addActionDatasetsForAutofillId(
+                        fillRequest,
+                        autofillId,
+                        primaryProviderInfo,
+                        fillResponseBuilder
+                    )
+                }
+            }
         }
-        if (!validFillResponse) {
-            return null
+        for (autofillId in uniqueAutofillIdsForRequest) {
+            addMoreOptionsDataset(
+                fillRequest,
+                autofillId,
+                fillResponseBuilder,
+                getCredResponse.intent.putExtra(
+                    ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, ArrayList(candidateProviders)
+                )
+            )
         }
         fillResponseBuilder.setClientState(responseClientState)
         return fillResponseBuilder.build()
     }
 
-    private fun processProvidersForAutofillId(
-            filLRequest: FillRequest,
-            autofillId: AutofillId,
-            providerDataList: ArrayList<GetCredentialProviderData>,
-            entryIconMap: Map<String, Icon>,
-            fillResponseBuilder: FillResponse.Builder,
-            bottomSheetIntent: Intent,
-            typePriorityMap: Map<String, Int>,
+    private fun addActionDatasetsForAutofillId(
+        fillRequest: FillRequest,
+        autofillId: AutofillId,
+        primaryProvider: ProviderInfo,
+        fillResponseBuilder: FillResponse.Builder,
+    ): Boolean {
+        var index = 0
+        var datasetAdded = false
+        primaryProvider.actionEntryList.forEach { actionEntry ->
+            if (index >= maxDatasetDisplayLimit(primaryProvider.actionEntryList.size)) {
+                return@forEach
+            }
+            val pendingIntent = actionEntry.pendingIntent
+            if (pendingIntent == null) {
+                Log.e(TAG, "Pending intent for action chip is null")
+                return@forEach
+            }
+
+            val icon: Icon? = Icon.createWithBitmap(actionEntry.icon.toBitmap())
+            if (icon == null) {
+                Log.e(TAG, "Icon for action chip is null")
+                return@forEach
+            }
+
+            val presentations = constructPresentations(
+                fillRequest,
+                index,
+                actionEntry,
+                pendingIntent,
+                icon,
+                actionEntry.title,
+                actionEntry.subTitle,
+                primaryProvider.actionEntryList.size
+            )
+
+            fillResponseBuilder.addDataset(
+                Dataset.Builder()
+                    .setField(
+                        autofillId,
+                        Field.Builder().setPresentations(presentations).build()
+                    )
+                    .setAuthentication(pendingIntent.intentSender)
+                    .build()
+            )
+            datasetAdded = true
+
+            index++
+        }
+
+        return datasetAdded
+    }
+
+    private fun addCredentialDatasetsForAutofillId(
+        fillRequest: FillRequest,
+        autofillId: AutofillId,
+        providerDataList: List<GetCredentialProviderData>,
+        entryIconMap: Map<String, Icon>,
+        fillResponseBuilder: FillResponse.Builder,
+        typePriorityMap: Map<String, Int>,
     ): Boolean {
         val providerList = GetFlowUtils.toProviderList(
             providerDataList,
-            this@CredentialAutofillService)
+            this@CredentialAutofillService
+        )
         if (providerList.isEmpty()) {
             return false
         }
         val providerDisplayInfo: ProviderDisplayInfo =
-                toProviderDisplayInfo(providerList, typePriorityMap)
+            toProviderDisplayInfo(providerList, typePriorityMap)
         var totalEntryCount = providerDisplayInfo.sortedUserNameToCredentialEntryList.size
-        val inlineSuggestionsRequest = filLRequest.inlineSuggestionsRequest
-        val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs
-        val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0
-        val maxDatasetDisplayLimit = this.resources.getInteger(
-                com.android.credentialmanager.R.integer.autofill_max_visible_datasets)
-                .coerceAtMost(totalEntryCount)
+
         var i = 0
         var datasetAdded = false
 
@@ -260,8 +345,6 @@
                 }
             }
         }
-        bottomSheetIntent.putExtra(
-                ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, providerDataList)
         providerDisplayInfo.sortedUserNameToCredentialEntryList.forEach usernameLoop@{
             val primaryEntry = it.sortedCredentialEntryList.first()
             val pendingIntent = primaryEntry.pendingIntent
@@ -271,7 +354,7 @@
                 Log.e(TAG, "PendingIntent was missing from the entry.")
                 return@usernameLoop
             }
-            if (i >= maxDatasetDisplayLimit) {
+            if (i >= maxDatasetDisplayLimit(totalEntryCount)) {
                 return@usernameLoop
             }
             val icon: Icon = if (primaryEntry.icon == null) {
@@ -280,116 +363,172 @@
                 getDefaultIcon()
             } else {
                 entryIconMap[primaryEntry.entryKey + primaryEntry.entrySubkey]
-                        ?: getDefaultIcon()
+                    ?: getDefaultIcon()
             }
-            // Create inline presentation
-            var inlinePresentation: InlinePresentation? = null
-            if (inlinePresentationSpecs != null && i < maxDatasetDisplayLimit) {
-                val spec: InlinePresentationSpec? = if (i < inlinePresentationSpecsCount) {
-                    inlinePresentationSpecs[i]
-                } else {
-                    inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
-                }
-                if (spec != null) {
-                    inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon,
-                            InlinePresentationsFactory.modifyInlinePresentationSpec
-                            (this@CredentialAutofillService, spec),
-                            duplicateDisplayNamesForPasskeys)
-                }
+            val displayName = primaryEntry.displayName
+            val title: String = if (primaryEntry.credentialType == CredentialType.PASSKEY &&
+                displayName != null
+            ) {
+                displayName
+            } else {
+                primaryEntry.userName
             }
-            var dropdownPresentation: RemoteViews? = null
-            if (i < maxDatasetDisplayLimit) {
-                dropdownPresentation = RemoteViewsFactory.createDropdownPresentation(
-                    this, icon, primaryEntry, /*isFirstEntry= */ i == 0,
-                    /*isLastEntry= */ (totalEntryCount - i == 1))
+            val subtitle = if (primaryEntry.credentialType ==
+                CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[title] == true
+            ) {
+                primaryEntry.userName
+            } else {
+                null
             }
-
-            val dataSetBuilder = Dataset.Builder()
-            val presentationBuilder = Presentations.Builder()
-            if (dropdownPresentation != null) {
-                presentationBuilder.setMenuPresentation(dropdownPresentation)
-            }
-            if (inlinePresentation != null) {
-                presentationBuilder.setInlinePresentation(inlinePresentation)
-            }
-
+            val presentations =
+                constructPresentations(
+                    fillRequest, i, primaryEntry, pendingIntent,
+                    icon, title, subtitle, totalEntryCount
+                )
             fillResponseBuilder.addDataset(
-                    dataSetBuilder
-                            .setField(
-                                    autofillId,
-                                    Field.Builder().setPresentations(
-                                            presentationBuilder.build())
-                                            .build())
-                            .setAuthentication(pendingIntent.intentSender)
-                            .setCredentialFillInIntent(fillInIntent)
-                            .build())
+                Dataset.Builder()
+                    .setField(
+                        autofillId,
+                        Field.Builder().setPresentations(
+                            presentations
+                        )
+                            .build()
+                    )
+                    .setAuthentication(pendingIntent.intentSender)
+                    .setCredentialFillInIntent(fillInIntent)
+                    .build()
+            )
             datasetAdded = true
             i++
         }
-        val pinnedSpec = getLastInlinePresentationSpec(inlinePresentationSpecs,
-                inlinePresentationSpecsCount)
-        if (datasetAdded) {
-            addDropdownMoreOptionsPresentation(bottomSheetIntent, autofillId, fillResponseBuilder)
-            if (pinnedSpec != null) {
-                addPinnedInlineSuggestion(pinnedSpec, autofillId,
-                        fillResponseBuilder, bottomSheetIntent)
-            }
-        }
         return datasetAdded
     }
 
-    private fun createInlinePresentation(
-            primaryEntry: CredentialEntryInfo,
-            pendingIntent: PendingIntent,
-            icon: Icon,
-            spec: InlinePresentationSpec,
-            duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>
-    ): InlinePresentation {
-        val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY &&
-            primaryEntry.displayName != null) {
-            primaryEntry.displayName!!
-        } else {
-            primaryEntry.userName
+    private fun addMoreOptionsDataset(
+        fillRequest: FillRequest,
+        autofillId: AutofillId,
+        fillResponseBuilder: FillResponse.Builder,
+        bottomSheetIntent: Intent
+    ) {
+        val inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
+        val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs
+        val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0
+        val pinnedSpec = getLastInlinePresentationSpec(
+            inlinePresentationSpecs,
+            inlinePresentationSpecsCount
+        )
+        addDropdownMoreOptionsPresentation(bottomSheetIntent, autofillId, fillResponseBuilder)
+        if (pinnedSpec != null) {
+            addPinnedInlineSuggestion(
+                pinnedSpec, autofillId,
+                fillResponseBuilder, bottomSheetIntent
+            )
         }
+    }
+
+    private fun constructPresentations(
+        fillRequest: FillRequest,
+        index: Int,
+        entry: EntryInfo,
+        pendingIntent: PendingIntent,
+        icon: Icon,
+        title: String,
+        subtitle: String?,
+        totalEntryCount: Int
+    ): Presentations {
+        val inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
+        val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs
+        val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0
+
+        // Create inline presentation
+        var inlinePresentation: InlinePresentation? = null
+        if (inlinePresentationSpecs != null && index < maxDatasetDisplayLimit(totalEntryCount)) {
+            val spec: InlinePresentationSpec? = if (index < inlinePresentationSpecsCount) {
+                inlinePresentationSpecs[index]
+            } else {
+                inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
+            }
+            if (spec != null) {
+                inlinePresentation = createInlinePresentation(
+                    pendingIntent, icon,
+                    InlinePresentationsFactory.modifyInlinePresentationSpec
+                        (this@CredentialAutofillService, spec),
+                    title, subtitle, entry is ActionEntryInfo
+                )
+            }
+        }
+        var dropdownPresentation: RemoteViews? = null
+        if (index < maxDatasetDisplayLimit(totalEntryCount)) {
+            dropdownPresentation = RemoteViewsFactory.createDropdownPresentation(
+                this, icon, entry, /*isFirstEntry= */ index == 0,
+                /*isLastEntry= */ (totalEntryCount - index == 1)
+            )
+        }
+
+        val presentationBuilder = Presentations.Builder()
+        if (dropdownPresentation != null) {
+            presentationBuilder.setMenuPresentation(dropdownPresentation)
+        }
+        if (inlinePresentation != null) {
+            presentationBuilder.setInlinePresentation(inlinePresentation)
+        }
+        return presentationBuilder.build()
+    }
+
+    private fun maxDatasetDisplayLimit(totalEntryCount: Int) = this.resources.getInteger(
+        com.android.credentialmanager.R.integer.autofill_max_visible_datasets
+    ).coerceAtMost(totalEntryCount)
+
+    private fun createInlinePresentation(
+        pendingIntent: PendingIntent,
+        icon: Icon,
+        spec: InlinePresentationSpec,
+        title: String,
+        subtitle: String?,
+        isActionEntry: Boolean
+    ): InlinePresentation {
         val sliceBuilder = InlineSuggestionUi
-                .newContentBuilder(pendingIntent)
-                .setTitle(displayName)
+            .newContentBuilder(pendingIntent)
+            .setTitle(title)
         icon.setTintBlendMode(BlendMode.DST)
         sliceBuilder.setStartIcon(icon)
-        if (primaryEntry.credentialType ==
-                CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) {
-            sliceBuilder.setSubtitle(primaryEntry.userName)
+        if (subtitle != null && !isActionEntry) {
+            sliceBuilder.setSubtitle(subtitle)
         }
         return InlinePresentation(
-                sliceBuilder.build().slice, spec, /* pinned= */ false)
+            sliceBuilder.build().slice, spec, /* pinned= */ false
+        )
     }
 
     private fun addDropdownMoreOptionsPresentation(
-            bottomSheetIntent: Intent,
-            autofillId: AutofillId,
-            fillResponseBuilder: FillResponse.Builder
+        bottomSheetIntent: Intent,
+        autofillId: AutofillId,
+        fillResponseBuilder: FillResponse.Builder
     ) {
         val presentationBuilder = Presentations.Builder()
-                .setMenuPresentation(
-                        RemoteViewsFactory.createMoreSignInOptionsPresentation(this))
+            .setMenuPresentation(
+                RemoteViewsFactory.createMoreSignInOptionsPresentation(this)
+            )
         val pendingIntent = setUpBottomSheetPendingIntent(bottomSheetIntent)
 
         fillResponseBuilder.addDataset(
-                Dataset.Builder()
-                        .setId(AutofillManager.PINNED_DATASET_ID)
-                        .setField(
-                                autofillId,
-                                Field.Builder().setPresentations(
-                                        presentationBuilder.build())
-                                        .build())
-                        .setAuthentication(pendingIntent.intentSender)
+            Dataset.Builder()
+                .setId(AutofillManager.PINNED_DATASET_ID)
+                .setField(
+                    autofillId,
+                    Field.Builder().setPresentations(
+                        presentationBuilder.build()
+                    )
                         .build()
+                )
+                .setAuthentication(pendingIntent.intentSender)
+                .build()
         )
     }
 
     private fun getLastInlinePresentationSpec(
-            inlinePresentationSpecs: List<InlinePresentationSpec>?,
-            inlinePresentationSpecsCount: Int
+        inlinePresentationSpecs: List<InlinePresentationSpec>?,
+        inlinePresentationSpecsCount: Int
     ): InlinePresentationSpec? {
         if (inlinePresentationSpecs != null) {
             return inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
@@ -398,40 +537,47 @@
     }
 
     private fun addPinnedInlineSuggestion(
-            spec: InlinePresentationSpec,
-            autofillId: AutofillId,
-            fillResponseBuilder: FillResponse.Builder,
-            bottomSheetIntent: Intent
+        spec: InlinePresentationSpec,
+        autofillId: AutofillId,
+        fillResponseBuilder: FillResponse.Builder,
+        bottomSheetIntent: Intent
     ) {
         val pendingIntent = setUpBottomSheetPendingIntent(bottomSheetIntent)
 
         val dataSetBuilder = Dataset.Builder()
         val sliceBuilder = InlineSuggestionUi
-                .newContentBuilder(pendingIntent)
-                .setStartIcon(Icon.createWithResource(this,
-                        com.android.credentialmanager.R.drawable.more_horiz_24px))
+            .newContentBuilder(pendingIntent)
+            .setStartIcon(
+                Icon.createWithResource(
+                    this,
+                    com.android.credentialmanager.R.drawable.more_horiz_24px
+                )
+            )
         val presentationBuilder = Presentations.Builder()
-                .setInlinePresentation(InlinePresentation(
-                        sliceBuilder.build().slice, spec, /* pinned= */ true))
+            .setInlinePresentation(
+                InlinePresentation(
+                    sliceBuilder.build().slice, spec, /* pinned= */ true
+                )
+            )
 
         fillResponseBuilder.addDataset(
-                dataSetBuilder
-                        .setId(AutofillManager.PINNED_DATASET_ID)
-                        .setField(
-                                autofillId,
-                                Field.Builder().setPresentations(
-                                        presentationBuilder.build()
-                                ).build()
-                        )
-                        .setAuthentication(pendingIntent.intentSender)
-                        .build()
+            dataSetBuilder
+                .setId(AutofillManager.PINNED_DATASET_ID)
+                .setField(
+                    autofillId,
+                    Field.Builder().setPresentations(
+                        presentationBuilder.build()
+                    ).build()
+                )
+                .setAuthentication(pendingIntent.intentSender)
+                .build()
         )
     }
 
     private fun setUpBottomSheetPendingIntent(intent: Intent): PendingIntent {
         intent.setAction(java.util.UUID.randomUUID().toString())
         return PendingIntent.getActivity(this, /*requestCode=*/0, intent,
-                PendingIntent.FLAG_MUTABLE, /*options=*/null)
+            PendingIntent.FLAG_MUTABLE, /*options=*/null)
     }
 
     /**
@@ -465,17 +611,33 @@
      *     }
      */
     private fun mapAutofillIdToProviders(
-        providerList: List<GetCredentialProviderData>
+        uniqueAutofillIdsForRequest: Set<AutofillId>,
+        providerList: List<GetCredentialProviderData>,
+        primaryProviderComponentName: ComponentName?
     ): Map<AutofillId, ArrayList<GetCredentialProviderData>> {
         val autofillIdToProviders: MutableMap<AutofillId, ArrayList<GetCredentialProviderData>> =
             mutableMapOf()
+        var primaryProvider: GetCredentialProviderData? = null
         providerList.forEach { provider ->
+            if (primaryProviderComponentName != null && Objects.equals(ComponentName
+                .unflattenFromString(provider
+                    .providerFlattenedComponentName), primaryProviderComponentName)) {
+                primaryProvider = provider
+            }
             val autofillIdToCredentialEntries:
                     MutableMap<AutofillId, ArrayList<Entry>> =
                 mapAutofillIdToCredentialEntries(provider.credentialEntries)
             autofillIdToCredentialEntries.forEach { (autofillId, entries) ->
                 autofillIdToProviders.getOrPut(autofillId) { ArrayList() }
-                        .add(copyProviderInfo(provider, entries))
+                    .add(copyProviderInfo(provider, entries))
+            }
+        }
+        // adds primary provider action entries for autofill IDs without credential entries
+        uniqueAutofillIdsForRequest.forEach { autofillId ->
+            if (!autofillIdToProviders.containsKey(autofillId) && primaryProvider != null) {
+                autofillIdToProviders.put(
+                    autofillId,
+                    ArrayList(listOf(copyProviderInfoForActionsOnly(primaryProvider!!))))
             }
         }
         return autofillIdToProviders
@@ -526,19 +688,35 @@
         )
     }
 
+    private fun copyProviderInfoForActionsOnly(
+        providerInfo: GetCredentialProviderData,
+    ): GetCredentialProviderData {
+        return GetCredentialProviderData(
+            providerInfo.providerFlattenedComponentName,
+            emptyList(),
+            providerInfo.actionChips,
+            emptyList(),
+            null
+        )
+    }
+
     override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
         TODO("Not yet implemented")
     }
 
     private fun getCredManRequest(
-            structure: AssistStructure,
-            sessionId: Int,
-            requestId: Int,
-            resultReceiver: ResultReceiver,
-            responseClientState: Bundle
+        structure: AssistStructure,
+        sessionId: Int,
+        requestId: Int,
+        resultReceiver: ResultReceiver,
+        responseClientState: Bundle,
+        uniqueAutofillIdsForRequest: MutableSet<AutofillId>
     ): GetCredentialRequest? {
         val credentialOptions: MutableList<CredentialOption> = mutableListOf()
-        traverseStructureForRequest(structure, credentialOptions, responseClientState, sessionId)
+        traverseStructureForRequest(
+            structure, credentialOptions, responseClientState,
+            sessionId, uniqueAutofillIdsForRequest
+        )
 
         if (credentialOptions.isNotEmpty()) {
             val dataBundle = Bundle()
@@ -558,7 +736,8 @@
             structure: AssistStructure,
             cmRequests: MutableList<CredentialOption>,
             responseClientState: Bundle,
-            sessionId: Int
+            sessionId: Int,
+            uniqueAutofillIdsForRequest: MutableSet<AutofillId>
     ) {
         val traversedViewNodes: MutableSet<AutofillId> = mutableSetOf()
         val credentialOptionsFromHints: MutableMap<String, CredentialOption> = mutableMapOf()
@@ -570,7 +749,7 @@
         windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
             traverseNodeForRequest(
                 windowNode.rootViewNode, cmRequests, responseClientState, traversedViewNodes,
-                credentialOptionsFromHints, sessionId)
+                credentialOptionsFromHints, sessionId, uniqueAutofillIdsForRequest)
         }
     }
 
@@ -580,7 +759,8 @@
             responseClientState: Bundle,
             traversedViewNodes: MutableSet<AutofillId>,
             credentialOptionsFromHints: MutableMap<String, CredentialOption>,
-            sessionId: Int
+            sessionId: Int,
+            uniqueAutofillIdsForRequest: MutableSet<AutofillId>
     ) {
         viewNode.autofillId?.let {
             val domain = viewNode.webDomain
@@ -590,7 +770,9 @@
                     WEBVIEW_REQUESTED_CREDENTIAL_KEY, true)
             }
             cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState,
-                traversedViewNodes, credentialOptionsFromHints, sessionId))
+                traversedViewNodes, credentialOptionsFromHints, sessionId,
+                uniqueAutofillIdsForRequest)
+            )
             traversedViewNodes.add(it)
         }
 
@@ -600,8 +782,10 @@
                 }
 
         children.forEach { childNode: AssistStructure.ViewNode ->
-            traverseNodeForRequest(childNode, cmRequests, responseClientState, traversedViewNodes,
-                credentialOptionsFromHints, sessionId)
+            traverseNodeForRequest(
+                childNode, cmRequests, responseClientState, traversedViewNodes,
+                credentialOptionsFromHints, sessionId, uniqueAutofillIdsForRequest
+            )
         }
     }
 
@@ -611,7 +795,8 @@
             responseClientState: Bundle,
             traversedViewNodes: MutableSet<AutofillId>,
             credentialOptionsFromHints: MutableMap<String, CredentialOption>,
-            sessionId: Int
+            sessionId: Int,
+            uniqueAutofillIdsForRequest: MutableSet<AutofillId>
     ): MutableList<CredentialOption> {
         val credentialOptions: MutableList<CredentialOption> = mutableListOf()
         if (Flags.autofillCredmanDevIntegration() && viewNode.pendingCredentialRequest != null) {
@@ -641,8 +826,9 @@
                                 CredentialProviderService.EXTRA_AUTOFILL_ID,
                                 associatedAutofillIds
                             )
+                            uniqueAutofillIdsForRequest.addAll(associatedAutofillIds)
                         }
-            }
+                }
         }
         // TODO(b/325502552): clean up cred option logic in autofill hint
         val credentialHints: MutableList<String> = mutableListOf()
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index 7bb08d2..98e1690 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -21,8 +21,9 @@
 import com.android.credentialmanager.common.Constants
 import android.widget.RemoteViews
 import androidx.core.content.ContextCompat
+import com.android.credentialmanager.model.get.ActionEntryInfo
 import com.android.credentialmanager.model.get.CredentialEntryInfo
-import com.android.credentialmanager.model.CredentialType
+import com.android.credentialmanager.model.EntryInfo
 import android.graphics.drawable.Icon
 
 class RemoteViewsFactory {
@@ -39,37 +40,47 @@
         fun createDropdownPresentation(
             context: Context,
             icon: Icon,
-            credentialEntryInfo: CredentialEntryInfo,
+            entryInfo: EntryInfo,
             isFirstEntry: Boolean,
             isLastEntry: Boolean,
         ): RemoteViews {
             var layoutId: Int = com.android.credentialmanager.R.layout
                     .credman_dropdown_presentation_layout
             val remoteViews = RemoteViews(context.packageName, layoutId)
-            val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName
-            remoteViews.setTextViewText(android.R.id.text1, displayName)
-            val secondaryText = getSecondaryText(credentialEntryInfo)
-            if (secondaryText.isNullOrBlank()) {
-                Log.w(Constants.LOG_TAG, "Secondary text for dropdown is null")
-            } else {
-                remoteViews.setTextViewText(android.R.id.text2, secondaryText)
+            if (entryInfo is CredentialEntryInfo) {
+                val displayName = entryInfo.displayName ?: entryInfo.userName
+                remoteViews.setTextViewText(android.R.id.text1, displayName)
+                val secondaryText = getSecondaryText(entryInfo)
+                if (secondaryText.isNullOrBlank()) {
+                    Log.w(Constants.LOG_TAG, "Secondary text for dropdown credential entry is null")
+                } else {
+                    remoteViews.setTextViewText(android.R.id.text2, secondaryText)
+                }
+                remoteViews.setContentDescription(
+                    android.R.id.icon1, entryInfo
+                        .providerDisplayName
+                )
+            } else if (entryInfo is ActionEntryInfo) {
+                remoteViews.setTextViewText(android.R.id.text1, entryInfo.title)
+                remoteViews.setTextViewText(android.R.id.text2, entryInfo.subTitle)
             }
-            remoteViews.setImageViewIcon(android.R.id.icon1, icon);
+            remoteViews.setImageViewIcon(android.R.id.icon1, icon)
             remoteViews.setBoolean(
-                android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
+                android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true
+            )
             remoteViews.setInt(
                 android.R.id.icon1,
                 SET_MAX_HEIGHT_METHOD_NAME,
                 context.resources.getDimensionPixelSize(
-                    com.android.credentialmanager.R.dimen.autofill_icon_size));
-            remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo
-                    .providerDisplayName);
+                    com.android.credentialmanager.R.dimen.autofill_icon_size
+                )
+            )
             val drawableId =
                 if (isFirstEntry)
                     com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one else
                     com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_middle
             remoteViews.setInt(
-                android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId);
+                android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId)
             if (isFirstEntry) remoteViews.setViewPadding(
                 com.android.credentialmanager.R.id.credential_card,
                 /* left=*/0,
@@ -94,8 +105,8 @@
          * providerDisplayName. Both credential type and provider display name should not be empty.
          */
         private fun getSecondaryText(credentialEntryInfo: CredentialEntryInfo): String? {
-            return listOf(if (credentialEntryInfo.displayName != null
-                && (credentialEntryInfo.displayName != credentialEntryInfo.userName))
+            return listOf(if (credentialEntryInfo.displayName != null &&
+                (credentialEntryInfo.displayName != credentialEntryInfo.userName))
                 (credentialEntryInfo.userName) else null,
                 credentialEntryInfo.credentialTypeDisplayName,
                 credentialEntryInfo.providerDisplayName).filterNot { it.isNullOrBlank() }
@@ -113,16 +124,16 @@
                 com.android.credentialmanager
                         .R.string.dropdown_presentation_more_sign_in_options_text))
             remoteViews.setBoolean(
-                android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
+                android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true)
             remoteViews.setInt(
                 android.R.id.icon1,
                 SET_MAX_HEIGHT_METHOD_NAME,
                 context.resources.getDimensionPixelSize(
-                    com.android.credentialmanager.R.dimen.autofill_icon_size));
+                    com.android.credentialmanager.R.dimen.autofill_icon_size))
             val drawableId =
                 com.android.credentialmanager.R.drawable.more_options_list_item
             remoteViews.setInt(
-                android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId);
+                android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId)
             return remoteViews
         }
     }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 8e78861..19f5a99 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -292,12 +292,12 @@
         providerDisplayInfo.remoteEntry == null &&
         providerDisplayInfo.authenticationEntryList.all { it.isUnlockedAndEmpty })
         GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY
+    else if (isRequestForAllOptions)
+        GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY
     else if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
         providerDisplayInfo.authenticationEntryList.isEmpty() &&
         providerDisplayInfo.remoteEntry != null)
         GetScreenState.REMOTE_ONLY
-    else if (isRequestForAllOptions)
-        GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY
     else if (isBiometricFlow(providerDisplayInfo, isFlowAutoSelectable(providerDisplayInfo)))
         GetScreenState.BIOMETRIC_SELECTION
     else GetScreenState.PRIMARY_SELECTION
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index d969d1c..2e9b7b4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -446,6 +446,9 @@
         // Returns InstallUserActionRequired stage if install details could be successfully
         // computed, else it returns InstallAborted.
         val confirmationSnippet: InstallStage = generateConfirmationSnippet()
+        if (confirmationSnippet.stageCode == InstallStage.STAGE_ABORTED) {
+            return confirmationSnippet
+        }
 
         val existingUpdateOwner: CharSequence? = getExistingUpdateOwner(newPackageInfo!!)
         return if (sessionId == SessionInfo.INVALID_ID &&
diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml
index 7c76ea1..221e8f5 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml
@@ -38,7 +38,7 @@
     <color name="settingslib_track_off_color">@color/settingslib_materialColorSurfaceContainerHighest</color>
 
     <!-- Dialog background color. -->
-    <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceInverse</color>
+    <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceContainer</color>
 
     <color name="settingslib_colorSurfaceHeader">@color/settingslib_materialColorSurfaceVariant</color>
 
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml
index 2a6499a..dc2d3dc 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml
@@ -38,7 +38,7 @@
     <color name="settingslib_track_off_color">@color/settingslib_materialColorSurfaceContainerHighest</color>
 
     <!-- Dialog background color. -->
-    <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceInverse</color>
+    <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceContainer</color>
 
     <!-- Material next track outline color-->
     <color name="settingslib_track_online_color">@color/settingslib_switch_track_outline_color</color>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 3dffb27..8917412 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -10,8 +10,10 @@
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothStatusCodes;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -37,12 +39,9 @@
 import com.android.settingslib.widget.AdaptiveIcon;
 import com.android.settingslib.widget.AdaptiveOutlineDrawable;
 
-import com.google.common.collect.ImmutableSet;
-
 import java.io.IOException;
 import java.util.List;
 import java.util.Locale;
-import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -56,8 +55,6 @@
     public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
     private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
     private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
-    private static final Set<String> EXCLUSIVE_MANAGERS =
-            ImmutableSet.of("com.google.android.gms.dck");
 
     private static ErrorListener sErrorListener;
 
@@ -740,14 +737,13 @@
 
     /**
      * Returns the BluetoothDevice's exclusive manager ({@link
-     * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the given
-     * set, otherwise null.
+     * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists, otherwise null.
      */
     @Nullable
-    private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) {
-        byte[] exclusiveManagerNameBytes =
+    private static String getExclusiveManager(BluetoothDevice bluetoothDevice) {
+        byte[] exclusiveManagerBytes =
                 bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
-        if (exclusiveManagerNameBytes == null) {
+        if (exclusiveManagerBytes == null) {
             Log.d(
                     TAG,
                     "Bluetooth device "
@@ -755,47 +751,46 @@
                             + " doesn't have exclusive manager");
             return null;
         }
-        String exclusiveManagerName = new String(exclusiveManagerNameBytes);
-        return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName : null;
+        return new String(exclusiveManagerBytes);
     }
 
-    /** Checks if given package is installed */
-    private static boolean isPackageInstalled(Context context, String packageName) {
+    /** Checks if given package is installed and enabled */
+    private static boolean isPackageInstalledAndEnabled(Context context, String packageName) {
         PackageManager packageManager = context.getPackageManager();
         try {
-            packageManager.getPackageInfo(packageName, 0);
-            return true;
+            ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
+            return appInfo.enabled;
         } catch (PackageManager.NameNotFoundException e) {
-            Log.d(TAG, "Package " + packageName + " is not installed");
+            Log.d(TAG, "Package " + packageName + " is not installed/enabled");
         }
         return false;
     }
 
     /**
      * A BluetoothDevice is exclusively managed if 1) it has field {@link
-     * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app name is
-     * in the allowlist. 3) the exclusive manager app is installed.
+     * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app is
+     * installed and enabled.
      */
     public static boolean isExclusivelyManagedBluetoothDevice(
             @NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) {
-        String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice);
+        String exclusiveManagerName = getExclusiveManager(bluetoothDevice);
         if (exclusiveManagerName == null) {
             return false;
         }
-        if (!isPackageInstalled(context, exclusiveManagerName)) {
+
+        ComponentName exclusiveManagerComponent =
+                ComponentName.unflattenFromString(exclusiveManagerName);
+        String exclusiveManagerPackage = exclusiveManagerComponent != null
+                ? exclusiveManagerComponent.getPackageName() : exclusiveManagerName;
+
+        if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) {
             return false;
         } else {
-            Log.d(TAG, "Found exclusively managed app " + exclusiveManagerName);
+            Log.d(TAG, "Found exclusively managed app " + exclusiveManagerPackage);
             return true;
         }
     }
 
-    /** Return the allowlist for exclusive manager names. */
-    @NonNull
-    public static Set<String> getExclusiveManagers() {
-        return EXCLUSIVE_MANAGERS;
-    }
-
     /**
      * Get CSIP group id for {@link CachedBluetoothDevice}.
      *
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index f197f9e..7a2818d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -28,7 +28,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.content.Context;
-import android.content.pm.PackageInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
@@ -80,7 +80,8 @@
     private static final String CONTROL_METADATA =
             "<HEARABLE_CONTROL_SLICE_WITH_WIDTH>" + STRING_METADATA
                     + "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>";
-    private static final String FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name";
+    private static final String TEST_EXCLUSIVE_MANAGER_PACKAGE = "com.test.manager";
+    private static final String TEST_EXCLUSIVE_MANAGER_COMPONENT = "com.test.manager/.component";
 
     @Before
     public void setUp() {
@@ -399,7 +400,7 @@
     }
 
     @Test
-    public void isExclusivelyManagedBluetoothDevice_isNotExclusivelyManaged_returnFalse() {
+    public void isExclusivelyManaged_hasNoManager_returnFalse() {
         when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
                 null);
 
@@ -408,45 +409,85 @@
     }
 
     @Test
-    public void isExclusivelyManagedBluetoothDevice_isNotInAllowList_returnFalse() {
+    public void isExclusivelyManaged_hasPackageName_packageNotInstalled_returnFalse()
+            throws Exception {
         when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
-                FAKE_EXCLUSIVE_MANAGER_NAME.getBytes());
+                TEST_EXCLUSIVE_MANAGER_PACKAGE.getBytes());
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager)
+                .getApplicationInfo(TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
 
         assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
                 mBluetoothDevice)).isEqualTo(false);
     }
 
     @Test
-    public void isExclusivelyManagedBluetoothDevice_packageNotInstalled_returnFalse()
+    public void isExclusivelyManaged_hasComponentName_packageNotInstalled_returnFalse()
             throws Exception {
-        final String exclusiveManagerName =
-                BluetoothUtils.getExclusiveManagers().stream().findAny().orElse(
-                        FAKE_EXCLUSIVE_MANAGER_NAME);
-
         when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
-                exclusiveManagerName.getBytes());
+                TEST_EXCLUSIVE_MANAGER_COMPONENT.getBytes());
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager).getPackageInfo(
-                exclusiveManagerName, 0);
+        doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager)
+                .getApplicationInfo(TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
 
         assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
-                mBluetoothDevice)).isEqualTo(false);
+            mBluetoothDevice)).isEqualTo(false);
     }
 
     @Test
-    public void isExclusivelyManagedBluetoothDevice_isExclusivelyManaged_returnTrue()
-            throws Exception {
-        final String exclusiveManagerName =
-                BluetoothUtils.getExclusiveManagers().stream().findAny().orElse(
-                        FAKE_EXCLUSIVE_MANAGER_NAME);
-
-        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
-                exclusiveManagerName.getBytes());
+    public void isExclusivelyManaged_hasPackageName_packageNotEnabled_returnFalse()
+             throws Exception {
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.enabled = false;
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0);
+        doReturn(appInfo).when(mPackageManager).getApplicationInfo(
+                TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+                TEST_EXCLUSIVE_MANAGER_PACKAGE.getBytes());
 
         assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
-                mBluetoothDevice)).isEqualTo(true);
+            mBluetoothDevice)).isEqualTo(false);
+    }
+
+    @Test
+    public void isExclusivelyManaged_hasComponentName_packageNotEnabled_returnFalse()
+            throws Exception {
+        ApplicationInfo appInfo = new ApplicationInfo();
+        appInfo.enabled = false;
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        doReturn(appInfo).when(mPackageManager).getApplicationInfo(
+                TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+                TEST_EXCLUSIVE_MANAGER_COMPONENT.getBytes());
+
+        assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+            mBluetoothDevice)).isEqualTo(false);
+    }
+
+    @Test
+    public void isExclusivelyManaged_hasPackageName_packageInstalledAndEnabled_returnTrue()
+            throws Exception {
+        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+                TEST_EXCLUSIVE_MANAGER_PACKAGE.getBytes());
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo(
+                TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+
+        assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+            mBluetoothDevice)).isEqualTo(true);
+    }
+
+    @Test
+    public void isExclusivelyManaged_hasComponentName_packageInstalledAndEnabled_returnTrue()
+            throws Exception {
+        when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+                TEST_EXCLUSIVE_MANAGER_COMPONENT.getBytes());
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo(
+                TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+
+        assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+            mBluetoothDevice)).isEqualTo(true);
     }
 
     @Test
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 461b6b3..70ce202 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2949,9 +2949,6 @@
         dumpSetting(s, p,
                 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
                 SystemSettingsProto.Screen.AUTO_BRIGHTNESS_ADJ);
-        dumpSetting(s, p,
-                Settings.System.SCREEN_BRIGHTNESS_FLOAT,
-                SystemSettingsProto.Screen.BRIGHTNESS_FLOAT);
         p.end(screenToken);
 
         dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index c891dfc..92167ee 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -935,7 +935,6 @@
                         Settings.System.VOLUME_VOICE, // deprecated since API 2?
                         Settings.System.WHEN_TO_MAKE_WIFI_CALLS, // bug?
                         Settings.System.WINDOW_ORIENTATION_LISTENER_LOG, // used for debugging only
-                        Settings.System.SCREEN_BRIGHTNESS_FLOAT,
                         Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
                         Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE,
                         Settings.System.WEAR_TTS_PREWARM_ENABLED,
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 65c5708..fce7a00f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -382,6 +382,7 @@
         "androidx.compose.material_material-icons-extended",
         "androidx.activity_activity-compose",
         "androidx.compose.animation_animation-graphics",
+        "device_policy_aconfig_flags_lib",
     ],
     libs: [
         "keepanno-annotations",
@@ -622,6 +623,7 @@
         "//frameworks/libs/systemui:compilelib",
         "SystemUI-tests-base",
         "androidx.compose.runtime_runtime",
+        "SystemUI-core",
     ],
     libs: [
         "keepanno-annotations",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 59e2b91..b9e70ef 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1110,7 +1110,7 @@
                 android:resource="@xml/home_controls_dream_metadata" />
         </service>
 
-        <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity"
+        <activity android:name="com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity"
             android:exported="false"
             android:theme="@style/ShortcutHelperTheme"
             android:excludeFromRecents="true"
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 14ebc39..755fe2a 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -4,6 +4,16 @@
 # NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
 
 flag {
+    name: "delay_show_magnification_button"
+    namespace: "accessibility"
+    description: "Delays the showing of magnification mode switch button."
+    bug: "338259519"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "floating_menu_animated_tuck"
     namespace: "accessibility"
     description: "Sets up animations for tucking/untucking and adjusts clipbounds."
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index de090f4..df44076 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -171,16 +171,6 @@
 }
 
 flag {
-    name: "nssl_falsing_fix"
-    namespace: "systemui"
-    description: "Minor touch changes to prevent falsing errors in NSSL"
-    bug: "316551193"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "refactor_get_current_user"
     namespace: "systemui"
     description: "KeyguardUpdateMonitor.getCurrentUser() was providing outdated results."
@@ -560,6 +550,13 @@
 }
 
 flag {
+    name: "enable_contextual_tip_for_mute_volume"
+    namespace: "systemui"
+    description: "Enables the contextual tip for muting the volume."
+    bug: "337737048"
+}
+
+flag {
    name: "disable_contextual_tips_frequency_check"
    description: "Disables frequency capping check for contextual tips."
    namespace: "systemui"
@@ -860,6 +857,16 @@
 }
 
 flag {
+   name: "restart_dream_on_unocclude"
+   namespace: "systemui"
+   description: "re-enters dreaming upon unocclude when dreaming when originally occluding"
+   bug: "338051457"
+   metadata {
+     purpose: PURPOSE_BUGFIX
+   }
+}
+
+flag {
   name: "communal_bouncer_do_not_modify_plugin_open"
   namespace: "systemui"
   description: "do not modify notification shade when handling bouncer expansion."
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 1e60b98..d4660fa 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -44,6 +44,7 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.policy.ScreenDecorationsUtils
 import com.android.systemui.Flags.activityTransitionUseLargestWindow
+import java.util.concurrent.Executor
 import kotlin.math.roundToInt
 
 private const val TAG = "ActivityTransitionAnimator"
@@ -52,14 +53,19 @@
  * A class that allows activities to be started in a seamless way from a view that is transforming
  * nicely into the starting window.
  */
-class ActivityTransitionAnimator(
+class ActivityTransitionAnimator
+@JvmOverloads
+constructor(
+    /** The executor that runs on the main thread. */
+    private val mainExecutor: Executor,
+
     /** The animator used when animating a View into an app. */
-    private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
+    private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
 
     /** The animator used when animating a Dialog into an app. */
     // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
     // TIMINGS.contentBeforeFadeOutDuration.
-    private val dialogToAppAnimator: TransitionAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
+    private val dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
 
     /**
      * Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -100,10 +106,6 @@
         // TODO(b/288507023): Remove this flag.
         @JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE
 
-        private val DEFAULT_TRANSITION_ANIMATOR = TransitionAnimator(TIMINGS, INTERPOLATORS)
-        private val DEFAULT_DIALOG_TO_APP_ANIMATOR =
-            TransitionAnimator(DIALOG_TIMINGS, INTERPOLATORS)
-
         /** Durations & interpolators for the navigation bar fading in & out. */
         private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
         private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
@@ -121,6 +123,14 @@
          * cancelled by WM.
          */
         private const val LONG_TRANSITION_TIMEOUT = 5_000L
+
+        private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator {
+            return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS)
+        }
+
+        private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator {
+            return TransitionAnimator(mainExecutor, DIALOG_TIMINGS, INTERPOLATORS)
+        }
     }
 
     /**
@@ -257,9 +267,7 @@
 
     private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
-            this.transitionContainer.context.mainExecutor.execute {
-                callOnIntentStartedOnMainThread(willAnimate)
-            }
+            mainExecutor.execute { callOnIntentStartedOnMainThread(willAnimate) }
         } else {
             if (DEBUG_TRANSITION_ANIMATION) {
                 Log.d(
@@ -479,12 +487,10 @@
         controller: Controller,
         callback: Callback,
         /** The animator to use to animate the window transition. */
-        transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
+        transitionAnimator: TransitionAnimator,
         /** Listener for animation lifecycle events. */
         listener: Listener? = null
     ) : IRemoteAnimationRunner.Stub() {
-        private val context = controller.transitionContainer.context
-
         // This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
         // etc.) are possible. So we need to make sure we drop any references that might
         // transitively cause leaks when we're done with animation.
@@ -493,11 +499,12 @@
         init {
             delegate =
                 AnimationDelegate(
+                    mainExecutor,
                     controller,
                     callback,
                     DelegatingAnimationCompletionListener(listener, this::dispose),
                     transitionAnimator,
-                    disableWmTimeout
+                    disableWmTimeout,
                 )
         }
 
@@ -510,7 +517,7 @@
             finishedCallback: IRemoteAnimationFinishedCallback?
         ) {
             val delegate = delegate
-            context.mainExecutor.execute {
+            mainExecutor.execute {
                 if (delegate == null) {
                     Log.i(TAG, "onAnimationStart called after completion")
                     // Animation started too late and timed out already. We need to still
@@ -525,7 +532,7 @@
         @BinderThread
         override fun onAnimationCancelled() {
             val delegate = delegate
-            context.mainExecutor.execute {
+            mainExecutor.execute {
                 delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion")
                 delegate?.onAnimationCancelled()
             }
@@ -535,19 +542,21 @@
         fun dispose() {
             // Drop references to animation controller once we're done with the animation
             // to avoid leaking.
-            context.mainExecutor.execute { delegate = null }
+            mainExecutor.execute { delegate = null }
         }
     }
 
     class AnimationDelegate
     @JvmOverloads
     constructor(
+        private val mainExecutor: Executor,
         private val controller: Controller,
         private val callback: Callback,
         /** Listener for animation lifecycle events. */
         private val listener: Listener? = null,
         /** The animator to use to animate the window transition. */
-        private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
+        private val transitionAnimator: TransitionAnimator =
+            defaultTransitionAnimator(mainExecutor),
 
         /**
          * Whether we should disable the WindowManager timeout. This should be set to true in tests
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index b89ebfc..f5d01d7 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -37,6 +37,7 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.util.maybeForceFullscreen
 import com.android.systemui.util.registerAnimationOnBackInvoked
+import java.util.concurrent.Executor
 import kotlin.math.roundToInt
 
 private const val TAG = "DialogTransitionAnimator"
@@ -55,10 +56,16 @@
 class DialogTransitionAnimator
 @JvmOverloads
 constructor(
+    private val mainExecutor: Executor,
     private val callback: Callback,
     private val interactionJankMonitor: InteractionJankMonitor,
     private val featureFlags: AnimationFeatureFlags,
-    private val transitionAnimator: TransitionAnimator = TransitionAnimator(TIMINGS, INTERPOLATORS),
+    private val transitionAnimator: TransitionAnimator =
+        TransitionAnimator(
+            mainExecutor,
+            TIMINGS,
+            INTERPOLATORS,
+        ),
     private val isForTesting: Boolean = false,
 ) {
     private companion object {
@@ -937,24 +944,9 @@
                 }
 
                 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
-                    // onLaunchAnimationEnd is called by an Animator at the end of the animation,
-                    // on a Choreographer animation tick. The following calls will move the animated
-                    // content from the dialog overlay back to its original position, and this
-                    // change must be reflected in the next frame given that we then sync the next
-                    // frame of both the content and dialog ViewRoots. However, in case that content
-                    // is rendered by Compose, whose compositions are also scheduled on a
-                    // Choreographer frame, any state change made *right now* won't be reflected in
-                    // the next frame given that a Choreographer frame can't schedule another and
-                    // have it happen in the same frame. So we post the forwarded calls to
-                    // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
-                    // that the move of the content back to its original window will be reflected in
-                    // the next frame right after [onLaunchAnimationEnd] is called.
-                    dialog.context.mainExecutor.execute {
-                        startController.onTransitionAnimationEnd(isExpandingFullyAbove)
-                        endController.onTransitionAnimationEnd(isExpandingFullyAbove)
-
-                        onLaunchAnimationEnd()
-                    }
+                    startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+                    endController.onTransitionAnimationEnd(isExpandingFullyAbove)
+                    onLaunchAnimationEnd()
                 }
 
                 override fun onTransitionAnimationProgress(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 679c969..cc55df1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -31,12 +31,17 @@
 import androidx.annotation.VisibleForTesting
 import com.android.app.animation.Interpolators.LINEAR
 import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
+import java.util.concurrent.Executor
 import kotlin.math.roundToInt
 
 private const val TAG = "TransitionAnimator"
 
 /** A base class to animate a window (activity or dialog) launch to or return from a view . */
-class TransitionAnimator(private val timings: Timings, private val interpolators: Interpolators) {
+class TransitionAnimator(
+    private val mainExecutor: Executor,
+    private val timings: Timings,
+    private val interpolators: Interpolators,
+) {
     companion object {
         internal const val DEBUG = false
         private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
@@ -351,11 +356,27 @@
                     if (DEBUG) {
                         Log.d(TAG, "Animation ended")
                     }
-                    controller.onTransitionAnimationEnd(isExpandingFullyAbove)
-                    transitionContainerOverlay.remove(windowBackgroundLayer)
 
-                    if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
-                        openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+                    // onAnimationEnd is called at the end of the animation, on a Choreographer
+                    // animation tick. During dialog launches, the following calls will move the
+                    // animated content from the dialog overlay back to its original position, and
+                    // this change must be reflected in the next frame given that we then sync the
+                    // next frame of both the content and dialog ViewRoots. During SysUI activity
+                    // launches, we will instantly collapse the shade at the end of the transition.
+                    // However, if those are rendered by Compose, whose compositions are also
+                    // scheduled on a Choreographer frame, any state change made *right now* won't
+                    // be reflected in the next frame given that a Choreographer frame can't
+                    // schedule another and have it happen in the same frame. So we post the
+                    // forwarded calls to [Controller.onLaunchAnimationEnd] in the main executor,
+                    // leaving this Choreographer frame, ensuring that any state change applied by
+                    // onTransitionAnimationEnd() will be reflected in the same frame.
+                    mainExecutor.execute {
+                        controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+                        transitionContainerOverlay.remove(windowBackgroundLayer)
+
+                        if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+                            openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+                        }
                     }
                 }
             }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
index 3eb1b14..604b517 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
@@ -35,6 +35,7 @@
     val viewDisplayCutoutKeyguardStatusBarView: ViewDisplayCutout? = null,
 ) {
     fun width() = abs(right.value - left.value).dp
+    fun height() = abs(bottom.value - top.value).dp
 }
 
 enum class CutoutLocation {
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 79b57ca7..6fe5cef 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
@@ -59,7 +59,8 @@
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.outlined.Edit
 import androidx.compose.material.icons.outlined.TouchApp
 import androidx.compose.material.icons.outlined.Widgets
@@ -277,7 +278,6 @@
 
         if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
             Toolbar(
-                isDraggingToRemove = isDraggingToRemove,
                 setToolbarSize = { toolbarSize = it },
                 setRemoveButtonCoordinates = { removeButtonCoordinates = it },
                 onEditDone = onEditDone,
@@ -577,7 +577,6 @@
  */
 @Composable
 private fun Toolbar(
-    isDraggingToRemove: Boolean,
     removeEnabled: Boolean,
     onRemoveClicked: () -> Unit,
     setToolbarSize: (toolbarSize: IntSize) -> Unit,
@@ -591,7 +590,7 @@
             label = "RemoveButtonAlphaAnimation"
         )
 
-    Row(
+    Box(
         modifier =
             Modifier.fillMaxWidth()
                 .padding(
@@ -600,65 +599,54 @@
                     end = Dimensions.ToolbarPaddingHorizontal,
                 )
                 .onSizeChanged { setToolbarSize(it) },
-        horizontalArrangement = Arrangement.SpaceBetween,
-        verticalAlignment = Alignment.CenterVertically
     ) {
         val spacerModifier = Modifier.width(ButtonDefaults.IconSpacing)
-        Button(
-            onClick = onOpenWidgetPicker,
-            colors = filledButtonColors(),
-            contentPadding = Dimensions.ButtonPadding
-        ) {
-            Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text))
-            Spacer(spacerModifier)
-            Text(
-                text = stringResource(R.string.hub_mode_add_widget_button_text),
-            )
+
+        if (!removeEnabled) {
+            Button(
+                modifier = Modifier.align(Alignment.CenterStart),
+                onClick = onOpenWidgetPicker,
+                colors = filledButtonColors(),
+                contentPadding = Dimensions.ButtonPadding
+            ) {
+                Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text))
+                Spacer(spacerModifier)
+                Text(
+                    text = stringResource(R.string.hub_mode_add_widget_button_text),
+                )
+            }
         }
 
-        val colors = LocalAndroidColorScheme.current
-        if (isDraggingToRemove) {
+        if (removeEnabled) {
             Button(
-                // Button is disabled to make it non-clickable
-                enabled = false,
-                onClick = {},
-                colors =
-                    ButtonDefaults.buttonColors(
-                        disabledContainerColor = colors.primary,
-                        disabledContentColor = colors.onPrimary,
-                    ),
-                contentPadding = Dimensions.ButtonPadding,
-                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
-            ) {
-                RemoveButtonContent(spacerModifier)
-            }
-        } else {
-            OutlinedButton(
-                enabled = removeEnabled,
                 onClick = onRemoveClicked,
-                colors =
-                    ButtonDefaults.outlinedButtonColors(
-                        contentColor = colors.primary,
-                        disabledContentColor = colors.primary
-                    ),
-                border = BorderStroke(width = 1.0.dp, color = colors.primary),
+                colors = filledButtonColors(),
                 contentPadding = Dimensions.ButtonPadding,
                 modifier =
                     Modifier.graphicsLayer { alpha = removeButtonAlpha }
                         .onGloballyPositioned { setRemoveButtonCoordinates(it) }
+                        .align(Alignment.Center)
             ) {
                 RemoveButtonContent(spacerModifier)
             }
         }
 
-        Button(
-            onClick = onEditDone,
-            colors = filledButtonColors(),
-            contentPadding = Dimensions.ButtonPadding
-        ) {
-            Text(
-                text = stringResource(R.string.hub_mode_editing_exit_button_text),
-            )
+        if (!removeEnabled) {
+            Button(
+                modifier = Modifier.align(Alignment.CenterEnd),
+                onClick = onEditDone,
+                colors = filledButtonColors(),
+                contentPadding = Dimensions.ButtonPadding
+            ) {
+                Icon(
+                    Icons.Default.Check,
+                    stringResource(id = R.string.hub_mode_editing_exit_button_text)
+                )
+                Spacer(spacerModifier)
+                Text(
+                    text = stringResource(R.string.hub_mode_editing_exit_button_text),
+                )
+            }
         }
     }
 }
@@ -762,7 +750,7 @@
 
 @Composable
 private fun RemoveButtonContent(spacerModifier: Modifier) {
-    Icon(Icons.Outlined.Delete, stringResource(R.string.button_to_remove_widget))
+    Icon(Icons.Default.Close, stringResource(R.string.button_to_remove_widget))
     Spacer(spacerModifier)
     Text(
         text = stringResource(R.string.button_to_remove_widget),
@@ -804,6 +792,8 @@
         is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
         is CommunalContentModel.WidgetContent.DisabledWidget ->
             DisabledWidgetPlaceholder(model, viewModel, modifier)
+        is CommunalContentModel.WidgetContent.PendingWidget ->
+            PendingWidgetPlaceholder(model, modifier)
         is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, modifier)
         is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
         is CommunalContentModel.Tutorial -> TutorialContent(modifier)
@@ -929,36 +919,36 @@
                     Modifier.semantics {
                         contentDescription = accessibilityLabel
                         onClick(label = clickActionLabel, action = null)
-                            val deleteAction =
-                                CustomAccessibilityAction(removeWidgetActionLabel) {
-                                    contentListState.onRemove(index)
-                                    contentListState.onSaveList()
-                                    true
-                                }
-                            val selectWidgetAction =
-                                CustomAccessibilityAction(clickActionLabel) {
-                                    val currentWidgetKey =
-                                        index?.let {
-                                            keyAtIndexIfEditable(contentListState.list, index)
-                                        }
-                                    viewModel.setSelectedKey(currentWidgetKey)
-                                    true
-                                }
-
-                            val actions = mutableListOf(deleteAction, selectWidgetAction)
-
-                            if (selectedIndex != null && selectedIndex != index) {
-                                actions.add(
-                                    CustomAccessibilityAction(placeWidgetActionLabel) {
-                                        contentListState.onMove(selectedIndex!!, index)
-                                        contentListState.onSaveList()
-                                        viewModel.setSelectedKey(null)
-                                        true
+                        val deleteAction =
+                            CustomAccessibilityAction(removeWidgetActionLabel) {
+                                contentListState.onRemove(index)
+                                contentListState.onSaveList()
+                                true
+                            }
+                        val selectWidgetAction =
+                            CustomAccessibilityAction(clickActionLabel) {
+                                val currentWidgetKey =
+                                    index?.let {
+                                        keyAtIndexIfEditable(contentListState.list, index)
                                     }
-                                )
+                                viewModel.setSelectedKey(currentWidgetKey)
+                                true
                             }
 
-                            customActions = actions
+                        val actions = mutableListOf(deleteAction, selectWidgetAction)
+
+                        if (selectedIndex != null && selectedIndex != index) {
+                            actions.add(
+                                CustomAccessibilityAction(placeWidgetActionLabel) {
+                                    contentListState.onMove(selectedIndex!!, index)
+                                    contentListState.onSaveList()
+                                    viewModel.setSelectedKey(null)
+                                    true
+                                }
+                            )
+                        }
+
+                        customActions = actions
                     }
                 }
     ) {
@@ -1074,13 +1064,43 @@
         Image(
             painter = rememberDrawablePainter(icon.loadDrawable(context)),
             contentDescription = stringResource(R.string.icon_description_for_disabled_widget),
-            modifier = Modifier.size(48.dp),
+            modifier = Modifier.size(Dimensions.IconSize),
             colorFilter = ColorFilter.colorMatrix(Colors.DisabledColorFilter),
         )
     }
 }
 
 @Composable
+fun PendingWidgetPlaceholder(
+    model: CommunalContentModel.WidgetContent.PendingWidget,
+    modifier: Modifier = Modifier,
+) {
+    val context = LocalContext.current
+    val icon: Icon =
+        if (model.icon != null) {
+            Icon.createWithBitmap(model.icon)
+        } else {
+            Icon.createWithResource(context, android.R.drawable.sym_def_app_icon)
+        }
+
+    Column(
+        modifier =
+            modifier.background(
+                MaterialTheme.colorScheme.surfaceVariant,
+                RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+            ),
+        verticalArrangement = Arrangement.Center,
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Image(
+            painter = rememberDrawablePainter(icon.loadDrawable(context)),
+            contentDescription = stringResource(R.string.icon_description_for_pending_widget),
+            modifier = Modifier.size(Dimensions.IconSize),
+        )
+    }
+}
+
+@Composable
 private fun SmartspaceContent(
     model: CommunalContentModel.Smartspace,
     modifier: Modifier = Modifier,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index ec9136d..30b6c6c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutoutPadding
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -68,6 +69,8 @@
 import com.android.compose.modifiers.thenIf
 import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
+import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
 import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
@@ -152,6 +155,8 @@
     modifier: Modifier = Modifier,
     shadeSession: SaveableSession,
 ) {
+    val cutoutLocation = LocalDisplayCutout.current.location
+
     val brightnessMirrorShowing by viewModel.brightnessMirrorViewModel.isShowing.collectAsState()
     val contentAlpha by
         animateFloatAsState(
@@ -183,6 +188,9 @@
                     // scene (and not the one under it) during a scene transition.
                     Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
                 }
+                .thenIf(cutoutLocation != CutoutLocation.CENTER) {
+                    Modifier.displayCutoutPadding()
+                },
     ) {
         val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
         val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
@@ -320,7 +328,6 @@
                                 createTintedIconManager = createTintedIconManager,
                                 createBatteryMeterViewController = createBatteryMeterViewController,
                                 statusBarIconController = statusBarIconController,
-                                modifier = Modifier.padding(horizontal = 16.dp),
                             )
                     }
                     Spacer(modifier = Modifier.height(16.dp))
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 7eaebc2..ff9c5a5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -5,10 +5,13 @@
 import com.android.systemui.bouncer.ui.composable.Bouncer
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
 import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
 import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
@@ -38,6 +41,13 @@
     from(
         Scenes.Gone,
         to = Scenes.Shade,
+        key = GoneToSplitShade,
+    ) {
+        goneToSplitShadeTransition()
+    }
+    from(
+        Scenes.Gone,
+        to = Scenes.Shade,
         key = SlightlyFasterShadeCollapse,
     ) {
         goneToShadeTransition(durationScale = 0.9)
@@ -68,5 +78,9 @@
             Notifications.Elements.NotificationScrim,
             y = { Shade.Dimensions.ScrimOverscrollLimit }
         )
+        translate(
+            Shade.Elements.SplitShadeStartColumn,
+            y = { Shade.Dimensions.ScrimOverscrollLimit }
+        )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
new file mode 100644
index 0000000..4dc36d6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.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.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
+import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.ui.composable.QuickSettings
+import com.android.systemui.shade.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.goneToSplitShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    swipeSpec =
+        spring(
+            stiffness = Spring.StiffnessMediumLow,
+            visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
+        )
+    distance =
+        object : UserActionDistance {
+            override fun UserActionDistanceScope.absoluteDistance(
+                fromSceneSize: IntSize,
+                orientation: Orientation,
+            ): Float {
+                return fromSceneSize.height.toFloat() * 2 / 3f
+            }
+        }
+
+    fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) }
+
+    fractionRange(start = .33f) {
+        fade(ShadeHeader.Elements.Clock)
+        fade(ShadeHeader.Elements.CollapsedContentStart)
+        fade(ShadeHeader.Elements.CollapsedContentEnd)
+        fade(ShadeHeader.Elements.PrivacyChip)
+        fade(QuickSettings.Elements.SplitShadeQuickSettings)
+        fade(QuickSettings.Elements.FooterActions)
+        fade(Notifications.Elements.NotificationScrim)
+    }
+}
+
+private val DefaultDuration = 500.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 0bd38a1..709a416 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -50,6 +50,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
 import androidx.compose.ui.viewinterop.AndroidView
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexScenePicker
@@ -63,6 +64,7 @@
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
+import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.res.R
@@ -77,6 +79,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
 import com.android.systemui.statusbar.policy.Clock
+import kotlin.math.max
 
 object ShadeHeader {
     object Elements {
@@ -121,7 +124,11 @@
     }
 
     val cutoutWidth = LocalDisplayCutout.current.width()
+    val cutoutHeight = LocalDisplayCutout.current.height()
+    val cutoutTop = LocalDisplayCutout.current.top
     val cutoutLocation = LocalDisplayCutout.current.location
+    val horizontalPadding =
+        max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
 
     val useExpandedFormat by
         remember(cutoutLocation) {
@@ -140,7 +147,7 @@
         contents =
             listOf(
                 {
-                    Row {
+                    Row(modifier = Modifier.padding(horizontal = horizontalPadding)) {
                         Clock(
                             scale = 1f,
                             viewModel = viewModel,
@@ -157,7 +164,12 @@
                 },
                 {
                     if (isPrivacyChipVisible) {
-                        Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
+                        Box(
+                            modifier =
+                                Modifier.height(CollapsedHeight)
+                                    .fillMaxWidth()
+                                    .padding(horizontal = horizontalPadding)
+                        ) {
                             PrivacyChip(
                                 viewModel = viewModel,
                                 modifier = Modifier.align(Alignment.CenterEnd),
@@ -166,9 +178,13 @@
                     } else {
                         Row(
                             horizontalArrangement = Arrangement.End,
-                            modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+                            modifier =
+                                Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+                                    .padding(horizontal = horizontalPadding)
                         ) {
-                            SystemIconContainer {
+                            SystemIconContainer(
+                                modifier = Modifier.align(Alignment.CenterVertically)
+                            ) {
                                 when (LocalWindowSizeClass.current.widthSizeClass) {
                                     WindowWidthSizeClass.Medium,
                                     WindowWidthSizeClass.Expanded ->
@@ -206,7 +222,7 @@
 
         val screenWidth = constraints.maxWidth
         val cutoutWidthPx = cutoutWidth.roundToPx()
-        val height = CollapsedHeight.roundToPx()
+        val height = max(cutoutHeight + (cutoutTop * 2), CollapsedHeight).roundToPx()
         val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height)
 
         val startMeasurable = measurables[0][0]
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 4ad8b9f..10fe0cab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -31,6 +31,7 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutoutPadding
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -67,6 +68,9 @@
 import com.android.compose.modifiers.padding
 import com.android.compose.modifiers.thenIf
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
+import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
+import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.controls.ui.composable.MediaCarousel
 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
@@ -97,6 +101,7 @@
         val MediaCarousel = ElementKey("ShadeMediaCarousel")
         val BackgroundScrim =
             ElementKey("ShadeBackgroundScrim", scenePicker = LowestZIndexScenePicker)
+        val SplitShadeStartColumn = ElementKey("SplitShadeStartColumn")
     }
 
     object Dimensions {
@@ -206,6 +211,8 @@
     modifier: Modifier = Modifier,
     shadeSession: SaveableSession,
 ) {
+    val cutoutLocation = LocalDisplayCutout.current.location
+
     val maxNotifScrimTop = remember { mutableStateOf(0f) }
     val tileSquishiness by
         animateSceneFloatAsState(
@@ -241,19 +248,21 @@
                         Column(
                             horizontalAlignment = Alignment.CenterHorizontally,
                             modifier =
-                                Modifier.fillMaxWidth().thenIf(isClickable) {
-                                    Modifier.clickable(onClick = { viewModel.onContentClicked() })
-                                }
+                                Modifier.fillMaxWidth()
+                                    .thenIf(isClickable) {
+                                        Modifier.clickable(
+                                            onClick = { viewModel.onContentClicked() }
+                                        )
+                                    }
+                                    .thenIf(cutoutLocation != CutoutLocation.CENTER) {
+                                        Modifier.displayCutoutPadding()
+                                    },
                         ) {
                             CollapsedShadeHeader(
                                 viewModel = viewModel.shadeHeaderViewModel,
                                 createTintedIconManager = createTintedIconManager,
                                 createBatteryMeterViewController = createBatteryMeterViewController,
                                 statusBarIconController = statusBarIconController,
-                                modifier =
-                                    Modifier.padding(
-                                        horizontal = Shade.Dimensions.HorizontalPadding
-                                    )
                             )
                             Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
                                 QuickSettings(
@@ -312,6 +321,8 @@
     modifier: Modifier = Modifier,
     shadeSession: SaveableSession,
 ) {
+    val screenCornerRadius = LocalScreenCornerRadius.current
+
     val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
     val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
     val customizingAnimationDuration by
@@ -320,7 +331,11 @@
     val footerActionsViewModel =
         remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
     val tileSquishiness by
-        animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
+        animateSceneFloatAsState(
+            value = 1f,
+            key = QuickSettings.SharedValues.TilesSquishiness,
+            canOverflow = false,
+        )
     val unfoldTranslationXForStartSide by
         viewModel
             .unfoldTranslationX(
@@ -388,8 +403,7 @@
                 createBatteryMeterViewController = createBatteryMeterViewController,
                 statusBarIconController = statusBarIconController,
                 modifier =
-                    Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
-                        .then(brightnessMirrorShowingModifier)
+                    Modifier.then(brightnessMirrorShowingModifier)
                         .padding(
                             horizontal = { unfoldTranslationXForStartSide.roundToInt() },
                         )
@@ -398,9 +412,9 @@
             Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
                 Box(
                     modifier =
-                        Modifier.weight(1f).graphicsLayer {
-                            translationX = unfoldTranslationXForStartSide
-                        },
+                        Modifier.element(Shade.Elements.SplitShadeStartColumn)
+                            .weight(1f)
+                            .graphicsLayer { translationX = unfoldTranslationXForStartSide },
                 ) {
                     BrightnessMirror(
                         viewModel = viewModel.brightnessMirrorViewModel,
@@ -467,7 +481,7 @@
                     modifier =
                         Modifier.weight(1f)
                             .fillMaxHeight()
-                            .padding(bottom = navBarBottomHeight)
+                            .padding(end = screenCornerRadius / 2f, bottom = navBarBottomHeight)
                             .then(brightnessMirrorShowingModifier)
                 )
             }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
index 0f6d51d..79d17ef 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.component.anc.ui.composable
 
+import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -35,7 +36,6 @@
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import com.android.systemui.res.R
 import com.android.systemui.volume.panel.component.anc.ui.viewmodel.AncViewModel
@@ -81,11 +81,10 @@
                 onClick = onClick,
             )
             Text(
-                modifier = Modifier.clearAndSetSemantics {},
+                modifier = Modifier.clearAndSetSemantics {}.basicMarquee(),
                 text = label,
                 style = MaterialTheme.typography.labelMedium,
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
+                maxLines = 2,
             )
         }
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index a46f4e5..cb3867f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -34,12 +34,15 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CustomAccessibilityAction
 import androidx.compose.ui.semantics.ProgressBarRangeInfo
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.customActions
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.progressBarRangeInfo
 import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.unit.dp
 import com.android.compose.PlatformSlider
 import com.android.compose.PlatformSliderColors
@@ -60,14 +63,31 @@
     PlatformSlider(
         modifier =
             modifier.clearAndSetSemantics {
-                if (!state.isEnabled) disabled()
-                contentDescription =
-                    state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
-
-                // provide a not animated value to the a11y because it fails to announce the
-                // settled value when it changes rapidly.
                 if (state.isEnabled) {
-                    progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
+                    contentDescription = state.label
+                    state.a11yClickDescription?.let {
+                        customActions =
+                            listOf(
+                                CustomAccessibilityAction(
+                                    it,
+                                ) {
+                                    onIconTapped()
+                                    true
+                                }
+                            )
+                    }
+
+                    state.a11yStateDescription?.let { stateDescription = it }
+                        ?: run {
+                            // provide a not animated value to the a11y because it fails to announce
+                            // the settled value when it changes rapidly.
+                            progressBarRangeInfo =
+                                ProgressBarRangeInfo(state.value, state.valueRange)
+                        }
+                } else {
+                    disabled()
+                    contentDescription =
+                        state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
                 }
                 setProgress { targetValue ->
                     val targetDirection =
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
index c51e8b0..a602e25 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
@@ -28,7 +28,11 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
+import com.android.systemui.res.R
 import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
 import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
 import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
@@ -49,6 +53,7 @@
         }
     }
 
+    val accessibilityTitle = stringResource(R.string.accessibility_volume_settings)
     val state: VolumePanelState by viewModel.volumePanelState.collectAsState()
     val components by viewModel.componentsLayout.collectAsState(null)
 
@@ -56,12 +61,14 @@
         components?.let { componentsState ->
             Components(
                 componentsState,
-                modifier.padding(
-                    start = padding,
-                    top = padding,
-                    end = padding,
-                    bottom = 20.dp,
-                )
+                modifier
+                    .semantics { paneTitle = accessibilityTitle }
+                    .padding(
+                        start = padding,
+                        top = padding,
+                        end = padding,
+                        bottom = 20.dp,
+                    )
             )
         }
     }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index f539a23..bdeab79 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -28,16 +28,16 @@
 import android.util.AttributeSet
 import android.util.MathUtils.constrainedMap
 import android.util.TypedValue
-import android.view.View.MeasureSpec.EXACTLY
 import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
 import android.widget.TextView
 import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.TextAnimator
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.core.MessageBuffer
 import java.io.PrintWriter
@@ -47,11 +47,13 @@
 import kotlin.math.min
 
 /**
- * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
- * The time's text color is a gradient that changes its colors based on its controller.
+ * Displays the time with the hour positioned above the minutes (ie: 09 above 30 is 9:30). The
+ * time's text color is a gradient that changes its colors based on its controller.
  */
 @SuppressLint("AppCompatCustomView")
-class AnimatableClockView @JvmOverloads constructor(
+class AnimatableClockView
+@JvmOverloads
+constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
@@ -63,7 +65,9 @@
         get() = field ?: DEFAULT_LOGGER
     var messageBuffer: MessageBuffer
         get() = logger.buffer
-        set(value) { logger = Logger(value, TAG) }
+        set(value) {
+            logger = Logger(value, TAG)
+        }
 
     var hasCustomPositionUpdatedAnimation: Boolean = false
     var migratedClocks: Boolean = false
@@ -77,16 +81,13 @@
     private var format: CharSequence? = null
     private var descFormat: CharSequence? = null
 
-    @ColorInt
-    private var dozingColor = 0
-
-    @ColorInt
-    private var lockScreenColor = 0
+    @ColorInt private var dozingColor = 0
+    @ColorInt private var lockScreenColor = 0
 
     private var lineSpacingScale = 1f
     private val chargeAnimationDelay: Int
     private var textAnimator: TextAnimator? = null
-    private var onTextAnimatorInitialized: Runnable? = null
+    private var onTextAnimatorInitialized: ((TextAnimator) -> Unit)? = null
 
     private var translateForCenterAnimation = false
     private val parentWidth: Int
@@ -94,9 +95,11 @@
 
     // last text size which is not constrained by view height
     private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
-    @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
-        { layout, invalidateCb ->
-            TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) }
+
+    @VisibleForTesting
+    var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
+        TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb)
+    }
 
     // Used by screenshot tests to provide stability
     @VisibleForTesting var isAnimationEnabled: Boolean = true
@@ -109,40 +112,55 @@
         get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal
 
     /**
-     * The number of pixels below the baseline. For fonts that support languages such as
-     * Burmese, this space can be significant and should be accounted for when computing layout.
+     * The number of pixels below the baseline. For fonts that support languages such as Burmese,
+     * this space can be significant and should be accounted for when computing layout.
      */
-    val bottom get() = paint?.fontMetrics?.bottom ?: 0f
+    val bottom: Float
+        get() = paint?.fontMetrics?.bottom ?: 0f
 
     init {
-        val animatableClockViewAttributes = context.obtainStyledAttributes(
-            attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
-        )
+        val animatableClockViewAttributes =
+            context.obtainStyledAttributes(
+                attrs,
+                R.styleable.AnimatableClockView,
+                defStyleAttr,
+                defStyleRes
+            )
 
         try {
-            dozingWeightInternal = animatableClockViewAttributes.getInt(
-                R.styleable.AnimatableClockView_dozeWeight,
-                /* default = */ 100
-            )
-            lockScreenWeightInternal = animatableClockViewAttributes.getInt(
-                R.styleable.AnimatableClockView_lockScreenWeight,
-                /* default = */ 300
-            )
-            chargeAnimationDelay = animatableClockViewAttributes.getInt(
-                R.styleable.AnimatableClockView_chargeAnimationDelay, /* default = */ 200
-            )
+            dozingWeightInternal =
+                animatableClockViewAttributes.getInt(
+                    R.styleable.AnimatableClockView_dozeWeight,
+                    /* default = */ 100
+                )
+            lockScreenWeightInternal =
+                animatableClockViewAttributes.getInt(
+                    R.styleable.AnimatableClockView_lockScreenWeight,
+                    /* default = */ 300
+                )
+            chargeAnimationDelay =
+                animatableClockViewAttributes.getInt(
+                    R.styleable.AnimatableClockView_chargeAnimationDelay,
+                    /* default = */ 200
+                )
         } finally {
             animatableClockViewAttributes.recycle()
         }
 
-        val textViewAttributes = context.obtainStyledAttributes(
-            attrs, android.R.styleable.TextView,
-            defStyleAttr, defStyleRes
-        )
+        val textViewAttributes =
+            context.obtainStyledAttributes(
+                attrs,
+                android.R.styleable.TextView,
+                defStyleAttr,
+                defStyleRes
+            )
 
         try {
-            isSingleLineInternal = textViewAttributes.getBoolean(
-                android.R.styleable.TextView_singleLine, /* default = */ false)
+            isSingleLineInternal =
+                textViewAttributes.getBoolean(
+                    android.R.styleable.TextView_singleLine,
+                    /* default = */ false
+                )
         } finally {
             textViewAttributes.recycle()
         }
@@ -156,9 +174,7 @@
         refreshFormat()
     }
 
-    /**
-     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
-     */
+    /** Whether to use a bolded version based on the user specified fontWeightAdjustment. */
     fun useBoldedVersion(): Boolean {
         // "Bold text" fontWeightAdjustment is 300.
         return resources.configuration.fontWeightAdjustment > 100
@@ -169,25 +185,30 @@
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
         logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
-        // Setting text actually triggers a layout pass (because the text view is set to
-        // wrap_content width and TextView always relayouts for this). Avoid needless
-        // relayout if the text didn't actually change.
-        if (!TextUtils.equals(text, formattedText)) {
-            text = formattedText
-            logger.d({ "refreshTime: done setting new time text to: $str1" }) {
-                str1 = formattedText?.toString()
-            }
-            // Because the TextLayout may mutate under the hood as a result of the new text, we
-            // notify the TextAnimator that it may have changed and request a measure/layout. A
-            // crash will occur on the next invocation of setTextStyle if the layout is mutated
-            // without being notified TextInterpolator being notified.
-            if (layout != null) {
-                textAnimator?.updateLayout(layout)
-                logger.d("refreshTime: done updating textAnimator layout")
-            }
-            requestLayout()
-            logger.d("refreshTime: after requestLayout")
+
+        // Setting text actually triggers a layout pass in TextView (because the text view is set to
+        // wrap_content width and TextView always relayouts for this). This avoids needless relayout
+        // if the text didn't actually change.
+        if (TextUtils.equals(text, formattedText)) {
+            return
         }
+
+        text = formattedText
+        logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+            str1 = formattedText?.toString()
+        }
+
+        // Because the TextLayout may mutate under the hood as a result of the new text, we notify
+        // the TextAnimator that it may have changed and request a measure/layout. A crash will
+        // occur on the next invocation of setTextStyle if the layout is mutated without being
+        // notified TextInterpolator being notified.
+        if (layout != null) {
+            textAnimator?.updateLayout(layout)
+            logger.d("refreshTime: done updating textAnimator layout")
+        }
+
+        requestLayout()
+        logger.d("refreshTime: after requestLayout")
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
@@ -206,19 +227,27 @@
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         logger.d("onMeasure")
-        if (migratedClocks && !isSingleLineInternal &&
-                MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+
+        if (
+            migratedClocks &&
+                !isSingleLineInternal &&
+                MeasureSpec.getMode(heightMeasureSpec) == EXACTLY
+        ) {
             // Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
-            super.setTextSize(TypedValue.COMPLEX_UNIT_PX,
-                    min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F))
+            super.setTextSize(
+                TypedValue.COMPLEX_UNIT_PX,
+                min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F)
+            )
         }
 
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         val animator = textAnimator
         if (animator == null) {
-            textAnimator = textAnimatorFactory(layout, ::invalidate)
-            onTextAnimatorInitialized?.run()
-            onTextAnimatorInitialized = null
+            textAnimator =
+                textAnimatorFactory(layout, ::invalidate)?.also {
+                    onTextAnimatorInitialized?.invoke(it)
+                    onTextAnimatorInitialized = null
+                }
         } else {
             animator.updateLayout(layout)
         }
@@ -243,15 +272,13 @@
             canvas.translate(parentWidth / 4f, 0f)
         }
 
-        logger.d({ "onDraw($str1)"}) { str1 = text.toString() }
+        logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
         // intentionally doesn't call super.onDraw here or else the text will be rendered twice
         textAnimator?.draw(canvas)
         canvas.restore()
     }
 
     override fun invalidate() {
-        @Suppress("UNNECESSARY_SAFE_CALL")
-        // logger won't be initialized when called by TextView's constructor
         logger.d("invalidate")
         super.invalidate()
     }
@@ -325,6 +352,7 @@
         if (textAnimator == null) {
             return
         }
+
         logger.d("animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
@@ -348,10 +376,11 @@
     }
 
     fun animateCharge(isDozing: () -> Boolean) {
+        // Skip charge animation if dozing animation is already playing.
         if (textAnimator == null || textAnimator!!.isRunning()) {
-            // Skip charge animation if dozing animation is already playing.
             return
         }
+
         logger.d("animateCharge")
         val startAnimPhase2 = Runnable {
             setTextStyle(
@@ -409,10 +438,9 @@
 
     /**
      * Set text style with an optional animation.
-     *
-     * By passing -1 to weight, the view preserves its current weight.
-     * By passing -1 to textSize, the view preserves its current text size.
-     * By passing null to color, the view preserves its current color.
+     * - By passing -1 to weight, the view preserves its current weight.
+     * - By passing -1 to textSize, the view preserves its current text size.
+     * - By passing null to color, the view preserves its current color.
      *
      * @param weight text weight.
      * @param textSize font size.
@@ -428,8 +456,8 @@
         delay: Long,
         onAnimationEnd: Runnable?
     ) {
-        if (textAnimator != null) {
-            textAnimator?.setTextStyle(
+        textAnimator?.let {
+            it.setTextStyle(
                 weight = weight,
                 textSize = textSize,
                 color = color,
@@ -439,23 +467,24 @@
                 delay = delay,
                 onAnimationEnd = onAnimationEnd
             )
-            textAnimator?.glyphFilter = glyphFilter
-        } else {
-            // when the text animator is set, update its start values
-            onTextAnimatorInitialized = Runnable {
-                textAnimator?.setTextStyle(
-                    weight = weight,
-                    textSize = textSize,
-                    color = color,
-                    animate = false,
-                    duration = duration,
-                    interpolator = interpolator,
-                    delay = delay,
-                    onAnimationEnd = onAnimationEnd
-                )
-                textAnimator?.glyphFilter = glyphFilter
-            }
+            it.glyphFilter = glyphFilter
         }
+            ?: run {
+                // when the text animator is set, update its start values
+                onTextAnimatorInitialized = { textAnimator ->
+                    textAnimator.setTextStyle(
+                        weight = weight,
+                        textSize = textSize,
+                        color = color,
+                        animate = false,
+                        duration = duration,
+                        interpolator = interpolator,
+                        delay = delay,
+                        onAnimationEnd = onAnimationEnd
+                    )
+                    textAnimator.glyphFilter = glyphFilter
+                }
+            }
     }
 
     private fun setTextStyle(
@@ -483,12 +512,13 @@
     fun refreshFormat(use24HourFormat: Boolean) {
         Patterns.update(context)
 
-        format = when {
-            isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
-            !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
-            isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
-            else -> DOUBLE_LINE_FORMAT_12_HOUR
-        }
+        format =
+            when {
+                isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
+                !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
+                isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
+                else -> DOUBLE_LINE_FORMAT_12_HOUR
+            }
         logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
 
         descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
@@ -510,10 +540,10 @@
         pw.println("    time=$time")
     }
 
-    private val moveToCenterDelays
+    private val moveToCenterDelays: List<Int>
         get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
 
-    private val moveToSideDelays
+    private val moveToSideDelays: List<Int>
         get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
 
     /**
@@ -531,7 +561,7 @@
     fun offsetGlyphsForStepClockAnimation(
         clockStartLeft: Int,
         clockMoveDirection: Int,
-        moveFraction: Float
+        moveFraction: Float,
     ) {
         val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
         val currentMoveAmount = left - clockStartLeft
@@ -558,8 +588,8 @@
      *
      * @param distance is the total distance in pixels to offset the glyphs when animation
      *   completes. Negative distance means we are animating the position towards the center.
-     * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1
-     *   means it finished moving.
+     * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+     *   it finished moving.
      */
     fun offsetGlyphsForStepClockAnimation(
         distance: Float,
@@ -568,13 +598,17 @@
         for (i in 0 until NUM_DIGITS) {
             val dir = if (isLayoutRtl) -1 else 1
             val digitFraction =
-                getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
+                getDigitFraction(
+                    digit = i,
+                    isMovingToCenter = distance > 0,
+                    fraction = fraction,
+                )
             val moveAmountForDigit = dir * distance * digitFraction
             glyphOffsets[i] = moveAmountForDigit
 
             if (distance > 0) {
-                // If distance > 0 then we are moving from the left towards the center.
-                // We need ensure that the glyphs are offset to the initial position.
+                // If distance > 0 then we are moving from the left towards the center. We need to
+                // ensure that the glyphs are offset to the initial position.
                 glyphOffsets[i] -= dir * distance
             }
         }
@@ -582,27 +616,25 @@
     }
 
     private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
-        // The delay for the digit, in terms of fraction (i.e. the digit should not move
-        // during 0.0 - 0.1).
-        val digitInitialDelay =
-            if (isMovingToCenter) {
-                moveToCenterDelays[digit] * MOVE_DIGIT_STEP
-            } else {
-                moveToSideDelays[digit] * MOVE_DIGIT_STEP
-            }
+        // The delay for the digit, in terms of fraction.
+        // (i.e. the digit should not move during 0.0 - 0.1).
+        val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays
+        val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP
         return MOVE_INTERPOLATOR.getInterpolation(
-                constrainedMap(
-                    0.0f,
-                    1.0f,
-                    digitInitialDelay,
-                    digitInitialDelay + AVAILABLE_ANIMATION_TIME,
-                    fraction,
-                )
+            constrainedMap(
+                /* rangeMin= */ 0.0f,
+                /* rangeMax= */ 1.0f,
+                /* valueMin= */ digitInitialDelay,
+                /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+                /* value= */ fraction,
             )
+        )
     }
 
-    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
-    // This is an optimization to ensure we only recompute the patterns when the inputs change.
+    /**
+     * DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. This
+     * is a cache optimization to ensure we only recompute the patterns when the inputs change.
+     */
     private object Patterns {
         var sClockView12: String? = null
         var sClockView24: String? = null
@@ -610,21 +642,22 @@
 
         fun update(context: Context) {
             val locale = Locale.getDefault()
-            val res = context.resources
-            val clockView12Skel = res.getString(R.string.clock_12hr_format)
-            val clockView24Skel = res.getString(R.string.clock_24hr_format)
-            val key = locale.toString() + clockView12Skel + clockView24Skel
-            if (key == sCacheKey) return
-
-            val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel)
-            sClockView12 = clockView12
-
-            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
-            // format.  The following code removes the AM/PM indicator if we didn't want it.
-            if (!clockView12Skel.contains("a")) {
-                sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
+            val clockView12Skel = context.resources.getString(R.string.clock_12hr_format)
+            val clockView24Skel = context.resources.getString(R.string.clock_24hr_format)
+            val key = "$locale$clockView12Skel$clockView24Skel"
+            if (key == sCacheKey) {
+                return
             }
 
+            sClockView12 =
+                DateFormat.getBestDateTimePattern(locale, clockView12Skel).let {
+                    // CLDR insists on adding an AM/PM indicator even though it wasn't in the format
+                    // string. The following code removes the AM/PM indicator if we didn't want it.
+                    if (!clockView12Skel.contains("a")) {
+                        it.replace("a".toRegex(), "").trim { it <= ' ' }
+                    } else it
+                }
+
             sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
             sCacheKey = key
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 447c280..6c3f3c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -17,8 +17,11 @@
 
 package com.android.keyguard
 
+import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
 import android.content.res.Configuration
 import android.media.AudioManager
+import android.platform.test.annotations.EnableFlags
 import android.telephony.TelephonyManager
 import android.testing.TestableLooper.RunWithLooper
 import android.testing.TestableResources
@@ -148,6 +151,7 @@
     @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var postureController: DevicePostureController
+    @Mock private lateinit var devicePolicyManager: DevicePolicyManager
 
     @Captor
     private lateinit var swipeListenerArgumentCaptor:
@@ -273,6 +277,7 @@
                 mSelectedUserInteractor,
                 deviceProvisionedController,
                 faceAuthAccessibilityDelegate,
+                devicePolicyManager,
                 keyguardTransitionInteractor,
                 { primaryBouncerInteractor },
             ) {
@@ -934,6 +939,45 @@
         verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any())
     }
 
+    @Test
+    @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+    fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() {
+        val mainUserId = 10
+
+        underTest.showMessageForFailedUnlockAttempt(
+            /* userId = */ mainUserId,
+            /* expiringUserId = */ mainUserId,
+            /* mainUserId = */ mainUserId,
+            /* remainingBeforeWipe = */ 1,
+            /* failedAttempts = */ 1
+        )
+
+        verify(view)
+            .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY))
+    }
+
+    @Test
+    @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+    fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() {
+        val secondaryUserId = 10
+        val mainUserId = 0
+
+        underTest.showMessageForFailedUnlockAttempt(
+            /* userId = */ secondaryUserId,
+            /* expiringUserId = */ secondaryUserId,
+            /* mainUserId = */ mainUserId,
+            /* remainingBeforeWipe = */ 1,
+            /* failedAttempts = */ 1
+        )
+
+        verify(view)
+            .showAlmostAtWipeDialog(
+                any(),
+                any(),
+                eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER)
+            )
+    }
+
     private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener
         get() {
             underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 3d8159e..9c9ee53 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -66,7 +65,7 @@
 
             runCurrent()
 
-            Truth.assertThat(actualValue).isFalse()
+            assertThat(actualValue).isFalse()
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
new file mode 100644
index 0000000..ca824cb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.LeakCheck
+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.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+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.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayRepositoryTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val testUser = UserHandle.of(1)!!
+    private val testStartTime = LocalTime.MIDNIGHT
+    private val testEndTime = LocalTime.NOON
+    private val colorDisplayManager =
+        mock<ColorDisplayManager> {
+            whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+            whenever(isNightDisplayActivated).thenReturn(false)
+            whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+            whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+        }
+    private val locationController = FakeLocationController(LeakCheck())
+    private val nightDisplayListener = mock<NightDisplayListener>()
+    private val listenerBuilder =
+        mock<NightDisplayListenerModule.Builder> {
+            whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+            whenever(build()).thenReturn(nightDisplayListener)
+        }
+    private val globalSettings = kosmos.fakeGlobalSettings
+    private val secureSettings = kosmos.fakeSettings
+    private val testDispatcher = StandardTestDispatcher()
+    private val scope = TestScope(testDispatcher)
+    private val userScopedColorDisplayManager =
+        mock<UserScopedService<ColorDisplayManager>> {
+            whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+        }
+
+    private val underTest =
+        NightDisplayRepository(
+            testDispatcher,
+            scope.backgroundScope,
+            globalSettings,
+            secureSettings,
+            listenerBuilder,
+            userScopedColorDisplayManager,
+            locationController,
+        )
+
+    @Before
+    fun setup() {
+        enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+    }
+
+    @Test
+    fun nightDisplayState_matchesAutoMode() =
+        scope.runTest {
+            enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+            val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+            val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+
+            runCurrent()
+
+            verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+            val callback = callbackCaptor.value
+
+            assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_DISABLED)
+
+            callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+            assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+            callback.onCustomStartTimeChanged(testStartTime)
+            assertThat(lastState!!.startTime).isEqualTo(testStartTime)
+
+            callback.onCustomEndTimeChanged(testEndTime)
+            assertThat(lastState!!.endTime).isEqualTo(testEndTime)
+
+            callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+            assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+        }
+
+    @Test
+    fun nightDisplayState_matchesIsNightDisplayActivated() =
+        scope.runTest {
+            val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+
+            val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+            runCurrent()
+
+            verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+            val callback = callbackCaptor.value
+            assertThat(lastState!!.isActivated)
+                .isEqualTo(colorDisplayManager.isNightDisplayActivated)
+
+            callback.onActivated(true)
+            assertThat(lastState!!.isActivated).isTrue()
+
+            callback.onActivated(false)
+            assertThat(lastState!!.isActivated).isFalse()
+        }
+
+    @Test
+    fun nightDisplayState_matchesController_initiallyCustomAutoMode() =
+        scope.runTest {
+            whenever(colorDisplayManager.nightDisplayAutoMode)
+                .thenReturn(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+            val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+            runCurrent()
+
+            assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+        }
+
+    @Test
+    fun nightDisplayState_matchesController_initiallyTwilightAutoMode() =
+        scope.runTest {
+            whenever(colorDisplayManager.nightDisplayAutoMode)
+                .thenReturn(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+            val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+            runCurrent()
+
+            assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+        }
+
+    @Test
+    fun nightDisplayState_matchesForceAutoMode() =
+        scope.runTest {
+            enrollInForcedNightDisplayAutoMode(false, testUser)
+            val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+            runCurrent()
+
+            assertThat(lastState!!.shouldForceAutoMode).isEqualTo(false)
+
+            enrollInForcedNightDisplayAutoMode(true, testUser)
+            assertThat(lastState!!.shouldForceAutoMode).isEqualTo(true)
+        }
+
+    private fun enrollInForcedNightDisplayAutoMode(enroll: Boolean, userHandle: UserHandle) {
+        globalSettings.putString(
+            Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE,
+            if (enroll) NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+            else NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE
+        )
+        secureSettings.putIntForUser(
+            Settings.Secure.NIGHT_DISPLAY_AUTO_MODE,
+            if (enroll) NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET else NIGHT_DISPLAY_AUTO_MODE_RAW_SET,
+            userHandle.identifier
+        )
+    }
+
+    private companion object {
+        const val INITIALLY_FORCE_AUTO_MODE = false
+        const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+        const val NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE = "0"
+        const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+        const val NIGHT_DISPLAY_AUTO_MODE_RAW_SET = 0
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 81878aa..0c5e726 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.authentication.domain.interactor
 
 import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
@@ -32,6 +34,8 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -410,12 +414,16 @@
         }
 
     @Test
+    @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
     fun upcomingWipe() =
         testScope.runTest {
             val upcomingWipe by collectLastValue(underTest.upcomingWipe)
             kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
             val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
             val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+            kosmos.fakeUserRepository.asMainUser()
+            kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe =
+                FakeUserRepository.MAIN_USER_ID
 
             underTest.authenticate(correctPin)
             assertThat(upcomingWipe).isNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
index 2386957..7628deb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
@@ -50,6 +50,7 @@
     @Mock private lateinit var context: Context
     @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var handler: Handler
+    @Mock private lateinit var packageInstallerMonitor: PackageInstallerMonitor
 
     private lateinit var repository: PackageChangeRepository
     private lateinit var updateMonitor: PackageUpdateMonitor
@@ -60,19 +61,20 @@
             MockitoAnnotations.initMocks(this@PackageChangeRepositoryTest)
             whenever(context.packageManager).thenReturn(packageManager)
 
-            repository = PackageChangeRepositoryImpl { user ->
-                updateMonitor =
-                    PackageUpdateMonitor(
-                        user = user,
-                        bgDispatcher = testDispatcher,
-                        scope = applicationCoroutineScope,
-                        context = context,
-                        bgHandler = handler,
-                        logger = PackageUpdateLogger(logcatLogBuffer()),
-                        systemClock = fakeSystemClock,
-                    )
-                updateMonitor
-            }
+            repository =
+                PackageChangeRepositoryImpl(packageInstallerMonitor) { user ->
+                    updateMonitor =
+                        PackageUpdateMonitor(
+                            user = user,
+                            bgDispatcher = testDispatcher,
+                            scope = applicationCoroutineScope,
+                            context = context,
+                            bgHandler = handler,
+                            logger = PackageUpdateLogger(logcatLogBuffer()),
+                            systemClock = fakeSystemClock,
+                        )
+                    updateMonitor
+                }
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
new file mode 100644
index 0000000..5556b04
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.common.data.repository
+
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageInstaller.SessionInfo
+import android.graphics.Bitmap
+import android.os.fakeExecutorHandler
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.PackageInstallSession
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PackageInstallerMonitorTest : SysuiTestCase() {
+    @Mock private lateinit var packageInstaller: PackageInstaller
+    @Mock private lateinit var icon1: Bitmap
+    @Mock private lateinit var icon2: Bitmap
+    @Mock private lateinit var icon3: Bitmap
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val handler = kosmos.fakeExecutorHandler
+
+    private lateinit var session1: SessionInfo
+    private lateinit var session2: SessionInfo
+    private lateinit var session3: SessionInfo
+
+    private lateinit var defaultSessions: List<SessionInfo>
+
+    private lateinit var underTest: PackageInstallerMonitor
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        session1 =
+            SessionInfo().apply {
+                sessionId = 1
+                appPackageName = "pkg_name_1"
+                appIcon = icon1
+            }
+        session2 =
+            SessionInfo().apply {
+                sessionId = 2
+                appPackageName = "pkg_name_2"
+                appIcon = icon2
+            }
+        session3 =
+            SessionInfo().apply {
+                sessionId = 3
+                appPackageName = "pkg_name_3"
+                appIcon = icon3
+            }
+        defaultSessions = listOf(session1, session2)
+
+        whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+        whenever(packageInstaller.getSessionInfo(1)).thenReturn(session1)
+        whenever(packageInstaller.getSessionInfo(2)).thenReturn(session2)
+
+        underTest =
+            PackageInstallerMonitor(
+                handler,
+                kosmos.applicationCoroutineScope,
+                logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+                packageInstaller,
+            )
+    }
+
+    @Test
+    fun installSessions_callbacksRegisteredOnlyWhenFlowIsCollected() =
+        testScope.runTest {
+            // Verify callback not added before flow is collected
+            verify(packageInstaller, never()).registerSessionCallback(any(), eq(handler))
+
+            // Start collecting the flow
+            val job =
+                backgroundScope.launch {
+                    underTest.installSessionsForPrimaryUser.collect {
+                        // Do nothing with the value
+                    }
+                }
+            runCurrent()
+
+            // Verify callback added only after flow is collected
+            val callback =
+                withArgCaptor<PackageInstaller.SessionCallback> {
+                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+                }
+
+            // Verify callback not removed
+            verify(packageInstaller, never()).unregisterSessionCallback(any())
+
+            // Stop collecting the flow
+            job.cancel()
+            runCurrent()
+
+            // Verify callback removed once flow stops being collected
+            verify(packageInstaller).unregisterSessionCallback(eq(callback))
+        }
+
+    @Test
+    fun installSessions_newSessionsAreAdded() =
+        testScope.runTest {
+            val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+            assertThat(installSessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions)
+
+            val callback =
+                withArgCaptor<PackageInstaller.SessionCallback> {
+                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+                }
+
+            // New session added
+            whenever(packageInstaller.getSessionInfo(3)).thenReturn(session3)
+            callback.onCreated(3)
+            runCurrent()
+
+            // Verify flow updated with the new session
+            assertThat(installSessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions + session3)
+        }
+
+    @Test
+    fun installSessions_finishedSessionsAreRemoved() =
+        testScope.runTest {
+            val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+            assertThat(installSessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions)
+
+            val callback =
+                withArgCaptor<PackageInstaller.SessionCallback> {
+                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+                }
+
+            // Session 1 finished successfully
+            callback.onFinished(1, /* success = */ true)
+            runCurrent()
+
+            // Verify flow updated with session 1 removed
+            assertThat(installSessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions - session1)
+        }
+
+    @Test
+    fun installSessions_sessionsUpdatedOnBadgingChange() =
+        testScope.runTest {
+            val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+            assertThat(installSessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions)
+
+            val callback =
+                withArgCaptor<PackageInstaller.SessionCallback> {
+                    verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+                }
+
+            // App icon for session 1 updated
+            val newSession =
+                SessionInfo().apply {
+                    sessionId = 1
+                    appPackageName = "pkg_name_1"
+                    appIcon = mock()
+                }
+            whenever(packageInstaller.getSessionInfo(1)).thenReturn(newSession)
+            callback.onBadgingChanged(1)
+            runCurrent()
+
+            // Verify flow updated with the new session 1
+            assertThat(installSessions)
+                .comparingElementsUsing(represents)
+                .containsExactlyElementsIn(defaultSessions - session1 + newSession)
+        }
+
+    private val represents =
+        Correspondence.from<PackageInstallSession, SessionInfo>(
+            { actual, expected ->
+                actual?.sessionId == expected?.sessionId &&
+                    actual?.packageName == expected?.appPackageName &&
+                    actual?.icon == expected?.getAppIcon()
+            },
+            "represents",
+        )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
index def63ec..bfed33c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
@@ -16,12 +16,16 @@
 
 package com.android.systemui.communal
 
+import android.platform.test.annotations.EnableFlags
 import android.service.dream.dreamManager
 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.communal.domain.interactor.communalInteractor
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -62,6 +66,7 @@
                     powerInteractor = kosmos.powerInteractor,
                     keyguardInteractor = kosmos.keyguardInteractor,
                     keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
+                    communalInteractor = kosmos.communalInteractor,
                     dreamManager = dreamManager,
                     bgScope = kosmos.applicationCoroutineScope,
                 )
@@ -84,6 +89,32 @@
         }
 
     @Test
+    @EnableFlags(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE)
+    fun restartDreamingWhenTransitioningFromDreamingToOccludedToDreaming() =
+        testScope.runTest {
+            keyguardRepository.setDreaming(false)
+            powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+            whenever(dreamManager.canStartDreaming(/* isScreenOn = */ true)).thenReturn(true)
+            runCurrent()
+
+            verify(dreamManager, never()).startDream()
+
+            kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
+            kosmos.fakeKeyguardRepository.setDreaming(true)
+            runCurrent()
+
+            transition(from = KeyguardState.DREAMING, to = KeyguardState.OCCLUDED)
+            kosmos.fakeKeyguardRepository.setKeyguardOccluded(false)
+            kosmos.fakeKeyguardRepository.setDreaming(false)
+            runCurrent()
+
+            transition(from = KeyguardState.OCCLUDED, to = KeyguardState.DREAMING)
+            runCurrent()
+
+            verify(dreamManager).startDream()
+        }
+
+    @Test
     fun shouldNotStartDreamWhenIneligibleToDream() =
         testScope.runTest {
             keyguardRepository.setDreaming(false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index fe4d32d..6ce6cdb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -17,16 +17,18 @@
 package com.android.systemui.communal.data.repository
 
 import android.app.backup.BackupManager
-import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
 import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
 import android.content.ComponentName
 import android.content.applicationContext
+import android.graphics.Bitmap
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.shared.model.PackageInstallSession
 import com.android.systemui.communal.data.backup.CommunalBackupUtils
 import com.android.systemui.communal.data.db.CommunalItemRank
 import com.android.systemui.communal.data.db.CommunalWidgetDao
@@ -46,10 +48,10 @@
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 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.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
-import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runCurrent
@@ -58,7 +60,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
@@ -68,10 +69,10 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
-    @Mock private lateinit var appWidgetManager: AppWidgetManager
     @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
-    @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo
     @Mock private lateinit var providerInfoA: AppWidgetProviderInfo
+    @Mock private lateinit var providerInfoB: AppWidgetProviderInfo
+    @Mock private lateinit var providerInfoC: AppWidgetProviderInfo
     @Mock private lateinit var communalWidgetHost: CommunalWidgetHost
     @Mock private lateinit var communalWidgetDao: CommunalWidgetDao
     @Mock private lateinit var backupManager: BackupManager
@@ -79,9 +80,11 @@
     private lateinit var backupUtils: CommunalBackupUtils
     private lateinit var logBuffer: LogBuffer
     private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>>
+    private lateinit var fakeProviders: MutableStateFlow<Map<Int, AppWidgetProviderInfo?>>
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
+    private val packageChangeRepository = kosmos.fakePackageChangeRepository
 
     private val fakeAllowlist =
         listOf(
@@ -96,6 +99,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         fakeWidgets = MutableStateFlow(emptyMap())
+        fakeProviders = MutableStateFlow(emptyMap())
         logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
         backupUtils = CommunalBackupUtils(kosmos.applicationContext)
 
@@ -103,12 +107,11 @@
 
         overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray())
 
-        whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
         whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets)
+        whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders)
 
         underTest =
             CommunalWidgetRepositoryImpl(
-                Optional.of(appWidgetManager),
                 appWidgetHost,
                 testScope.backgroundScope,
                 kosmos.testDispatcher,
@@ -117,6 +120,7 @@
                 logBuffer,
                 backupManager,
                 backupUtils,
+                packageChangeRepository,
             )
     }
 
@@ -126,15 +130,13 @@
             val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
             val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
             fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
-            whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)
-
-            installedProviders(listOf(stopwatchProviderInfo))
+            fakeProviders.value = mapOf(1 to providerInfoA)
 
             val communalWidgets by collectLastValue(underTest.communalWidgets)
             verify(communalWidgetDao).getWidgets()
             assertThat(communalWidgets)
                 .containsExactly(
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = communalWidgetItemEntry.widgetId,
                         providerInfo = providerInfoA,
                         priority = communalItemRankEntry.rank,
@@ -146,6 +148,102 @@
         }
 
     @Test
+    fun communalWidgets_widgetsWithoutMatchingProvidersAreSkipped() =
+        testScope.runTest {
+            // Set up 4 widgets, but widget 3 and 4 don't have matching providers
+            fakeWidgets.value =
+                mapOf(
+                    CommunalItemRank(uid = 1L, rank = 1) to
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                    CommunalItemRank(uid = 2L, rank = 2) to
+                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+                    CommunalItemRank(uid = 3L, rank = 3) to
+                        CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L),
+                    CommunalItemRank(uid = 4L, rank = 4) to
+                        CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L),
+                )
+            fakeProviders.value =
+                mapOf(
+                    1 to providerInfoA,
+                    2 to providerInfoB,
+                )
+
+            // Expect to see only widget 1 and 2
+            val communalWidgets by collectLastValue(underTest.communalWidgets)
+            assertThat(communalWidgets)
+                .containsExactly(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        providerInfo = providerInfoA,
+                        priority = 1,
+                    ),
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 2,
+                        providerInfo = providerInfoB,
+                        priority = 2,
+                    ),
+                )
+        }
+
+    @Test
+    fun communalWidgets_updatedWhenProvidersUpdate() =
+        testScope.runTest {
+            // Set up widgets and providers
+            fakeWidgets.value =
+                mapOf(
+                    CommunalItemRank(uid = 1L, rank = 1) to
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                    CommunalItemRank(uid = 2L, rank = 2) to
+                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+                )
+            fakeProviders.value =
+                mapOf(
+                    1 to providerInfoA,
+                    2 to providerInfoB,
+                )
+
+            // Expect two widgets
+            val communalWidgets by collectLastValue(underTest.communalWidgets)
+            assertThat(communalWidgets).isNotNull()
+            assertThat(communalWidgets)
+                .containsExactly(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        providerInfo = providerInfoA,
+                        priority = 1,
+                    ),
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 2,
+                        providerInfo = providerInfoB,
+                        priority = 2,
+                    ),
+                )
+
+            // Provider info updated for widget 1
+            fakeProviders.value =
+                mapOf(
+                    1 to providerInfoC,
+                    2 to providerInfoB,
+                )
+            runCurrent()
+
+            assertThat(communalWidgets)
+                .containsExactly(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        // Verify that provider info updated
+                        providerInfo = providerInfoC,
+                        priority = 1,
+                    ),
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 2,
+                        providerInfo = providerInfoB,
+                        priority = 2,
+                    ),
+                )
+        }
+
+    @Test
     fun addWidget_allocateId_bindWidget_andAddToDb() =
         testScope.runTest {
             val provider = ComponentName("pkg_name", "cls_name")
@@ -434,9 +532,102 @@
             assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank)
         }
 
-    private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
-        whenever(appWidgetManager.installedProviders).thenReturn(providers)
-    }
+    @Test
+    fun pendingWidgets() =
+        testScope.runTest {
+            fakeWidgets.value =
+                mapOf(
+                    CommunalItemRank(uid = 1L, rank = 1) to
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                    CommunalItemRank(uid = 2L, rank = 2) to
+                        CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+                )
+
+            // Widget 1 is installed
+            fakeProviders.value = mapOf(1 to providerInfoA)
+
+            // Widget 2 is pending install
+            val fakeIcon = mock<Bitmap>()
+            packageChangeRepository.setInstallSessions(
+                listOf(
+                    PackageInstallSession(
+                        sessionId = 1,
+                        packageName = "pk_2",
+                        icon = fakeIcon,
+                        user = UserHandle.CURRENT,
+                    )
+                )
+            )
+
+            val communalWidgets by collectLastValue(underTest.communalWidgets)
+            assertThat(communalWidgets)
+                .containsExactly(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        providerInfo = providerInfoA,
+                        priority = 1,
+                    ),
+                    CommunalWidgetContentModel.Pending(
+                        appWidgetId = 2,
+                        priority = 2,
+                        packageName = "pk_2",
+                        icon = fakeIcon,
+                        user = UserHandle.CURRENT,
+                    ),
+                )
+        }
+
+    @Test
+    fun pendingWidgets_pendingWidgetBecomesAvailableAfterInstall() =
+        testScope.runTest {
+            fakeWidgets.value =
+                mapOf(
+                    CommunalItemRank(uid = 1L, rank = 1) to
+                        CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+                )
+
+            // Widget 1 is pending install
+            val fakeIcon = mock<Bitmap>()
+            packageChangeRepository.setInstallSessions(
+                listOf(
+                    PackageInstallSession(
+                        sessionId = 1,
+                        packageName = "pk_1",
+                        icon = fakeIcon,
+                        user = UserHandle.CURRENT,
+                    )
+                )
+            )
+
+            val communalWidgets by collectLastValue(underTest.communalWidgets)
+            assertThat(communalWidgets)
+                .containsExactly(
+                    CommunalWidgetContentModel.Pending(
+                        appWidgetId = 1,
+                        priority = 1,
+                        packageName = "pk_1",
+                        icon = fakeIcon,
+                        user = UserHandle.CURRENT,
+                    ),
+                )
+
+            // Package for widget 1 finished installing
+            packageChangeRepository.setInstallSessions(emptyList())
+
+            // Provider info for widget 1 becomes available
+            fakeProviders.value = mapOf(1 to providerInfoA)
+
+            runCurrent()
+
+            assertThat(communalWidgets)
+                .containsExactly(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        providerInfo = providerInfoA,
+                        priority = 1,
+                    ),
+                )
+        }
 
     private fun setAppWidgetIds(ids: List<Int>) {
         whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 456fb79..766798c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -23,6 +23,7 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.content.Intent
 import android.content.pm.UserInfo
+import android.graphics.Bitmap
 import android.os.UserHandle
 import android.os.UserManager
 import android.os.userManager
@@ -871,7 +872,14 @@
             // One widget is filtered out and the remaining two link to main user id.
             assertThat(checkNotNull(widgetContent).size).isEqualTo(2)
             widgetContent!!.forEachIndexed { _, model ->
-                assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
+                assertThat(model is CommunalContentModel.WidgetContent.Widget).isTrue()
+                assertThat(
+                        (model as CommunalContentModel.WidgetContent.Widget)
+                            .providerInfo
+                            .profile
+                            ?.identifier
+                    )
+                    .isEqualTo(MAIN_USER_INFO.id)
             }
         }
 
@@ -1037,9 +1045,9 @@
             runCurrent()
 
             val widgetContent by collectLastValue(underTest.widgetContent)
-            // Given three widgets, and one of them is associated with work profile.
+            // One available work widget, one pending work widget, and one regular available widget.
             val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
-            val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+            val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
             val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
             val widgets = listOf(widget1, widget2, widget3)
             widgetRepository.setCommunalWidgets(widgets)
@@ -1049,11 +1057,9 @@
                 DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
             )
 
-            // Widget under work profile is filtered out and the remaining two link to main user id.
-            assertThat(widgetContent).hasSize(2)
-            widgetContent!!.forEach { model ->
-                assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
-            }
+            // Widgets under work profile are filtered out. Only the regular widget remains.
+            assertThat(widgetContent).hasSize(1)
+            assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(3)
         }
 
     @Test
@@ -1076,7 +1082,7 @@
             val widgetContent by collectLastValue(underTest.widgetContent)
             // Given three widgets, and one of them is associated with work profile.
             val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
-            val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+            val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
             val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
             val widgets = listOf(widget1, widget2, widget3)
             widgetRepository.setCommunalWidgets(widgets)
@@ -1086,10 +1092,11 @@
                 DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE
             )
 
-            // Widget under work profile is available.
+            // Widgets under work profile are available.
             assertThat(widgetContent).hasSize(3)
-            assertThat(widgetContent!![0].providerInfo.profile?.identifier)
-                .isEqualTo(USER_INFO_WORK.id)
+            assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(1)
+            assertThat(widgetContent?.get(1)?.appWidgetId).isEqualTo(2)
+            assertThat(widgetContent?.get(2)?.appWidgetId).isEqualTo(3)
         }
 
     @Test
@@ -1182,8 +1189,11 @@
         )
     }
 
-    private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
-        mock<CommunalWidgetContentModel> {
+    private fun createWidgetForUser(
+        appWidgetId: Int,
+        userId: Int
+    ): CommunalWidgetContentModel.Available =
+        mock<CommunalWidgetContentModel.Available> {
             whenever(this.appWidgetId).thenReturn(appWidgetId)
             val providerInfo =
                 mock<AppWidgetProviderInfo>().apply {
@@ -1193,11 +1203,27 @@
             whenever(this.providerInfo).thenReturn(providerInfo)
         }
 
+    private fun createPendingWidgetForUser(
+        appWidgetId: Int,
+        priority: Int = 0,
+        packageName: String = "",
+        icon: Bitmap? = null,
+        userId: Int = 0,
+    ): CommunalWidgetContentModel.Pending {
+        return CommunalWidgetContentModel.Pending(
+            appWidgetId = appWidgetId,
+            priority = priority,
+            packageName = packageName,
+            icon = icon,
+            user = UserHandle(userId),
+        )
+    }
+
     private fun createWidgetWithCategory(
         appWidgetId: Int,
         category: Int
     ): CommunalWidgetContentModel =
-        mock<CommunalWidgetContentModel> {
+        mock<CommunalWidgetContentModel.Available> {
             whenever(this.appWidgetId).thenReturn(appWidgetId)
             val providerInfo = mock<AppWidgetProviderInfo>().apply { widgetCategory = category }
             whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
index 02d927a..f9d5073 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
@@ -86,4 +86,26 @@
             )
             assertThat(isUmoOnCommunal).isFalse()
         }
+
+    @Test
+    fun testIsUmoOnCommunalDuringTransitionBetweenOccludedAndGlanceableHub() =
+        testScope.runTest {
+            val isUmoOnCommunal by collectLastValue(underTest.isUmoOnCommunal)
+            assertThat(isUmoOnCommunal).isNull()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.OCCLUDED,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope
+            )
+            assertThat(isUmoOnCommunal).isTrue()
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                testScope
+            )
+
+            assertThat(isUmoOnCommunal).isFalse()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
index 89a4c04..b3a12a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,6 +38,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -96,4 +101,137 @@
 
             assertThat(appWidgetIdToRemove).isEqualTo(2)
         }
+
+    @Test
+    fun observer_onHostStartListeningTriggeredWhileObserverActive() =
+        testScope.runTest {
+            // Observer added
+            val observer = mock<CommunalAppWidgetHost.Observer>()
+            underTest.addObserver(observer)
+            runCurrent()
+
+            // Verify callback triggered
+            verify(observer, never()).onHostStartListening()
+            underTest.startListening()
+            runCurrent()
+            verify(observer).onHostStartListening()
+
+            clearInvocations(observer)
+
+            // Observer removed
+            underTest.removeObserver(observer)
+            runCurrent()
+
+            // Verify callback not triggered
+            underTest.startListening()
+            runCurrent()
+            verify(observer, never()).onHostStartListening()
+        }
+
+    @Test
+    fun observer_onHostStopListeningTriggeredWhileObserverActive() =
+        testScope.runTest {
+            // Observer added
+            val observer = mock<CommunalAppWidgetHost.Observer>()
+            underTest.addObserver(observer)
+            runCurrent()
+
+            // Verify callback triggered
+            verify(observer, never()).onHostStopListening()
+            underTest.stopListening()
+            runCurrent()
+            verify(observer).onHostStopListening()
+
+            clearInvocations(observer)
+
+            // Observer removed
+            underTest.removeObserver(observer)
+            runCurrent()
+
+            // Verify callback not triggered
+            underTest.stopListening()
+            runCurrent()
+            verify(observer, never()).onHostStopListening()
+        }
+
+    @Test
+    fun observer_onAllocateAppWidgetIdTriggeredWhileObserverActive() =
+        testScope.runTest {
+            // Observer added
+            val observer = mock<CommunalAppWidgetHost.Observer>()
+            underTest.addObserver(observer)
+            runCurrent()
+
+            // Verify callback triggered
+            verify(observer, never()).onAllocateAppWidgetId(any())
+            val id = underTest.allocateAppWidgetId()
+            runCurrent()
+            verify(observer).onAllocateAppWidgetId(eq(id))
+
+            clearInvocations(observer)
+
+            // Observer removed
+            underTest.removeObserver(observer)
+            runCurrent()
+
+            // Verify callback not triggered
+            underTest.allocateAppWidgetId()
+            runCurrent()
+            verify(observer, never()).onAllocateAppWidgetId(any())
+        }
+
+    @Test
+    fun observer_onDeleteAppWidgetIdTriggeredWhileObserverActive() =
+        testScope.runTest {
+            // Observer added
+            val observer = mock<CommunalAppWidgetHost.Observer>()
+            underTest.addObserver(observer)
+            runCurrent()
+
+            // Verify callback triggered
+            verify(observer, never()).onDeleteAppWidgetId(any())
+            underTest.deleteAppWidgetId(1)
+            runCurrent()
+            verify(observer).onDeleteAppWidgetId(eq(1))
+
+            clearInvocations(observer)
+
+            // Observer removed
+            underTest.removeObserver(observer)
+            runCurrent()
+
+            // Verify callback not triggered
+            underTest.deleteAppWidgetId(2)
+            runCurrent()
+            verify(observer, never()).onDeleteAppWidgetId(any())
+        }
+
+    @Test
+    fun observer_multipleObservers() =
+        testScope.runTest {
+            // Set up two observers
+            val observer1 = mock<CommunalAppWidgetHost.Observer>()
+            val observer2 = mock<CommunalAppWidgetHost.Observer>()
+            underTest.addObserver(observer1)
+            underTest.addObserver(observer2)
+            runCurrent()
+
+            // Verify both observers triggered
+            verify(observer1, never()).onHostStartListening()
+            verify(observer2, never()).onHostStartListening()
+            underTest.startListening()
+            runCurrent()
+            verify(observer1).onHostStartListening()
+            verify(observer2).onHostStartListening()
+
+            // Observer 1 removed
+            underTest.removeObserver(observer1)
+            runCurrent()
+
+            // Verify only observer 2 is triggered
+            underTest.stopListening()
+            runCurrent()
+            verify(observer2).onHostStopListening()
+            verify(observer1, never()).onHostStopListening()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 9aebc30..6ca04df 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -123,12 +123,12 @@
             // Widgets available.
             val widgets =
                 listOf(
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 0,
                         priority = 30,
                         providerInfo = providerInfo,
                     ),
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 1,
                         priority = 20,
                         providerInfo = providerInfo,
@@ -177,12 +177,12 @@
             // Widgets available.
             val widgets =
                 listOf(
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 0,
                         priority = 30,
                         providerInfo = providerInfo,
                     ),
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 1,
                         priority = 20,
                         providerInfo = providerInfo,
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 569116c..1f8cb8a 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
@@ -90,7 +90,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     @Mock private lateinit var mediaHost: MediaHost
     @Mock private lateinit var user: UserInfo
     @Mock private lateinit var providerInfo: AppWidgetProviderInfo
@@ -111,7 +111,7 @@
     private lateinit var underTest: CommunalViewModel
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
@@ -186,12 +186,12 @@
             // Widgets available.
             val widgets =
                 listOf(
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 0,
                         priority = 30,
                         providerInfo = providerInfo,
                     ),
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 1,
                         priority = 20,
                         providerInfo = providerInfo,
@@ -245,7 +245,7 @@
 
             widgetRepository.setCommunalWidgets(
                 listOf(
-                    CommunalWidgetContentModel(
+                    CommunalWidgetContentModel.Available(
                         appWidgetId = 1,
                         priority = 1,
                         providerInfo = providerInfo,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 6cae5d3..3d2eabf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -18,6 +18,7 @@
 
 import android.appwidget.AppWidgetProviderInfo
 import android.content.pm.UserInfo
+import android.graphics.Bitmap
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -60,6 +61,7 @@
     private val kosmos = testKosmos()
 
     @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+    @Mock private lateinit var communalWidgetHost: CommunalWidgetHost
 
     private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>
 
@@ -78,6 +80,7 @@
         underTest =
             CommunalAppWidgetHostStartable(
                 appWidgetHost,
+                communalWidgetHost,
                 kosmos.communalInteractor,
                 kosmos.fakeUserTracker,
                 kosmos.applicationCoroutineScope,
@@ -143,16 +146,44 @@
         }
 
     @Test
+    fun observeHostWhenCommunalIsAvailable() =
+        with(kosmos) {
+            testScope.runTest {
+                setCommunalAvailable(true)
+                communalInteractor.setEditModeOpen(false)
+                verify(communalWidgetHost, never()).startObservingHost()
+                verify(communalWidgetHost, never()).stopObservingHost()
+
+                underTest.start()
+                runCurrent()
+
+                verify(communalWidgetHost).startObservingHost()
+                verify(communalWidgetHost, never()).stopObservingHost()
+
+                setCommunalAvailable(false)
+                runCurrent()
+
+                verify(communalWidgetHost).stopObservingHost()
+            }
+        }
+
+    @Test
     fun removeAppWidgetReportedByHost() =
         with(kosmos) {
             testScope.runTest {
                 // Set up communal widgets
                 val widget1 =
-                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) }
+                    mock<CommunalWidgetContentModel.Available> {
+                        whenever(this.appWidgetId).thenReturn(1)
+                    }
                 val widget2 =
-                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) }
+                    mock<CommunalWidgetContentModel.Available> {
+                        whenever(this.appWidgetId).thenReturn(2)
+                    }
                 val widget3 =
-                    mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) }
+                    mock<CommunalWidgetContentModel.Available> {
+                        whenever(this.appWidgetId).thenReturn(3)
+                    }
                 fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
 
                 underTest.start()
@@ -184,8 +215,9 @@
                     userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK),
                     selectedUserIndex = 0,
                 )
+                // One work widget, one pending work widget, and one personal widget.
                 val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
-                val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+                val widget2 = createPendingWidgetForUser(2, USER_INFO_WORK.id)
                 val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
                 val widgets = listOf(widget1, widget2, widget3)
                 fakeCommunalWidgetRepository.setCommunalWidgets(widgets)
@@ -209,8 +241,8 @@
                 fakeKeyguardRepository.setKeyguardShowing(true)
                 runCurrent()
 
-                // Widget created for work profile is removed.
-                assertThat(communalWidgets).containsExactly(widget2, widget3)
+                // Both work widgets are removed.
+                assertThat(communalWidgets).containsExactly(widget3)
             }
         }
 
@@ -227,14 +259,32 @@
             )
         }
 
-    private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
-        mock<CommunalWidgetContentModel> {
+    private fun createWidgetForUser(
+        appWidgetId: Int,
+        userId: Int
+    ): CommunalWidgetContentModel.Available =
+        mock<CommunalWidgetContentModel.Available> {
             whenever(this.appWidgetId).thenReturn(appWidgetId)
             val providerInfo = mock<AppWidgetProviderInfo>()
             whenever(providerInfo.profile).thenReturn(UserHandle(userId))
             whenever(this.providerInfo).thenReturn(providerInfo)
         }
 
+    private fun createPendingWidgetForUser(
+        appWidgetId: Int,
+        userId: Int,
+        priority: Int = 0,
+        packageName: String = "",
+        icon: Bitmap? = null,
+    ): CommunalWidgetContentModel.Pending =
+        CommunalWidgetContentModel.Pending(
+            appWidgetId = appWidgetId,
+            priority = priority,
+            packageName = packageName,
+            icon = icon,
+            user = UserHandle(userId),
+        )
+
     private companion object {
         val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
         val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
index 88f5e1b..054e516 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.widgets
 
+import android.appwidget.AppWidgetHost
 import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
@@ -26,6 +27,8 @@
 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.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
@@ -40,6 +43,7 @@
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -47,6 +51,8 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -59,6 +65,10 @@
 
     @Mock private lateinit var appWidgetManager: AppWidgetManager
     @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+    @Mock private lateinit var providerInfo1: AppWidgetProviderInfo
+    @Mock private lateinit var providerInfo2: AppWidgetProviderInfo
+    @Mock private lateinit var providerInfo3: AppWidgetProviderInfo
+
     private val selectedUserInteractor: SelectedUserInteractor by lazy {
         kosmos.selectedUserInteractor
     }
@@ -69,8 +79,19 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        whenever(
+                appWidgetManager.bindAppWidgetIdIfAllowed(
+                    any<Int>(),
+                    any<UserHandle>(),
+                    any<ComponentName>(),
+                    any<Bundle>()
+                )
+            )
+            .thenReturn(true)
+
         underTest =
             CommunalWidgetHost(
+                kosmos.applicationCoroutineScope,
                 Optional.of(appWidgetManager),
                 appWidgetHost,
                 selectedUserInteractor,
@@ -89,15 +110,6 @@
 
             val user = UserHandle(checkNotNull(userId))
             whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId)
-            whenever(
-                    appWidgetManager.bindAppWidgetIdIfAllowed(
-                        any<Int>(),
-                        any<UserHandle>(),
-                        any<ComponentName>(),
-                        any<Bundle>(),
-                    )
-                )
-                .thenReturn(true)
 
             // bind the widget with the current user when no user is explicitly set
             val result = underTest.allocateIdAndBindWidget(provider)
@@ -121,15 +133,6 @@
             val user = UserHandle(0)
 
             whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId)
-            whenever(
-                    appWidgetManager.bindAppWidgetIdIfAllowed(
-                        any<Int>(),
-                        any<UserHandle>(),
-                        any<ComponentName>(),
-                        any<Bundle>()
-                    )
-                )
-                .thenReturn(true)
 
             // provider and user handle are both set
             val result = underTest.allocateIdAndBindWidget(provider, user)
@@ -172,6 +175,261 @@
             assertThat(result).isNull()
         }
 
+    @Test
+    fun listener_exactlyOneListenerRegisteredForEachWidgetWhenHostStartListening() =
+        testScope.runTest {
+            // 3 widgets registered with the host
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3))
+
+            underTest.startObservingHost()
+            runCurrent()
+
+            // Make sure no listener is set before host starts listening
+            verify(appWidgetHost, never()).setListener(any(), any())
+
+            // Host starts listening
+            val observer =
+                withArgCaptor<CommunalAppWidgetHost.Observer> {
+                    verify(appWidgetHost).addObserver(capture())
+                }
+            observer.onHostStartListening()
+            runCurrent()
+
+            // Verify a listener is set for each widget
+            verify(appWidgetHost, times(3)).setListener(any(), any())
+            verify(appWidgetHost).setListener(eq(1), any())
+            verify(appWidgetHost).setListener(eq(2), any())
+            verify(appWidgetHost).setListener(eq(3), any())
+        }
+
+    @Test
+    fun listener_listenersRemovedWhenHostStopListening() =
+        testScope.runTest {
+            // 3 widgets registered with the host
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3))
+
+            underTest.startObservingHost()
+            runCurrent()
+
+            // Host starts listening
+            val observer =
+                withArgCaptor<CommunalAppWidgetHost.Observer> {
+                    verify(appWidgetHost).addObserver(capture())
+                }
+            observer.onHostStartListening()
+            runCurrent()
+
+            // Verify none of the listener is removed before host stop listening
+            verify(appWidgetHost, never()).removeListener(any())
+
+            observer.onHostStopListening()
+
+            // Verify each listener is removed
+            verify(appWidgetHost, times(3)).removeListener(any())
+            verify(appWidgetHost).removeListener(eq(1))
+            verify(appWidgetHost).removeListener(eq(2))
+            verify(appWidgetHost).removeListener(eq(3))
+        }
+
+    @Test
+    fun listener_addNewListenerWhenNewIdAllocated() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf())
+            val observer = start()
+
+            // Verify no listener is set before a new app widget id is allocated
+            verify(appWidgetHost, never()).setListener(any(), any())
+
+            // Allocate an app widget id
+            observer.onAllocateAppWidgetId(1)
+
+            // Verify new listener set for that app widget id
+            verify(appWidgetHost).setListener(eq(1), any())
+        }
+
+    @Test
+    fun listener_removeListenerWhenWidgetDeleted() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1))
+            val observer = start()
+
+            // Verify listener not removed before widget deleted
+            verify(appWidgetHost, never()).removeListener(eq(1))
+
+            // Widget deleted
+            observer.onDeleteAppWidgetId(1)
+
+            // Verify listener removed for that widget
+            verify(appWidgetHost).removeListener(eq(1))
+        }
+
+    @Test
+    fun providerInfo_populatesWhenStartListening() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+            whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+            whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+            val providerInfoValues by collectValues(underTest.appWidgetProviders)
+
+            // Assert that the map is empty before host starts listening
+            assertThat(providerInfoValues).hasSize(1)
+            assertThat(providerInfoValues[0]).isEmpty()
+
+            start()
+            runCurrent()
+
+            // Assert that the provider info map is populated after host started listening, and that
+            // all providers are emitted at once
+            assertThat(providerInfoValues).hasSize(2)
+            assertThat(providerInfoValues[1])
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo1),
+                        Pair(2, providerInfo2),
+                    )
+                )
+        }
+
+    @Test
+    fun providerInfo_clearsWhenStopListening() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+            whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+            whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+            val observer = start()
+            runCurrent()
+
+            // Assert that the provider info map is populated
+            val providerInfo by collectLastValue(underTest.appWidgetProviders)
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo1),
+                        Pair(2, providerInfo2),
+                    )
+                )
+
+            // Host stop listening
+            observer.onHostStopListening()
+
+            // Assert that the provider info map is cleared
+            assertThat(providerInfo).isEmpty()
+        }
+
+    @Test
+    fun providerInfo_onUpdate() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+            whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+            whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+            val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+            start()
+            runCurrent()
+
+            // Assert that the provider info map is populated
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo1),
+                        Pair(2, providerInfo2),
+                    )
+                )
+
+            // Provider info for widget 1 updated
+            val listener =
+                withArgCaptor<AppWidgetHost.AppWidgetHostListener> {
+                    verify(appWidgetHost).setListener(eq(1), capture())
+                }
+            listener.onUpdateProviderInfo(providerInfo3)
+            runCurrent()
+
+            // Assert that the update is reflected in the flow
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo3),
+                        Pair(2, providerInfo2),
+                    )
+                )
+        }
+
+    @Test
+    fun providerInfo_updateWhenANewWidgetIsBound() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+            whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+            whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+            val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+            start()
+            runCurrent()
+
+            // Assert that the provider info map is populated
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo1),
+                        Pair(2, providerInfo2),
+                    )
+                )
+
+            // Bind a new widget
+            whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(3)
+            whenever(appWidgetManager.getAppWidgetInfo(3)).thenReturn(providerInfo3)
+            val newWidgetComponentName = ComponentName.unflattenFromString("pkg_new/cls_new")!!
+            underTest.allocateIdAndBindWidget(newWidgetComponentName)
+            runCurrent()
+
+            // Assert that the new provider is reflected in the flow
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo1),
+                        Pair(2, providerInfo2),
+                        Pair(3, providerInfo3),
+                    )
+                )
+        }
+
+    @Test
+    fun providerInfo_updateWhenWidgetRemoved() =
+        testScope.runTest {
+            whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+            whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+            whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+            val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+            val observer = start()
+            runCurrent()
+
+            // Assert that the provider info map is populated
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(1, providerInfo1),
+                        Pair(2, providerInfo2),
+                    )
+                )
+
+            // Remove widget 1
+            observer.onDeleteAppWidgetId(1)
+            runCurrent()
+
+            // Assert that provider info for widget 1 is removed
+            assertThat(providerInfo)
+                .containsExactlyEntriesIn(
+                    mapOf(
+                        Pair(2, providerInfo2),
+                    )
+                )
+        }
+
     private fun selectUser() {
         kosmos.fakeUserRepository.selectedUser.value =
             SelectedUserModel(
@@ -179,4 +437,16 @@
                 selectionStatus = SelectionStatus.SELECTION_COMPLETE
             )
     }
+
+    private fun TestScope.start(): CommunalAppWidgetHost.Observer {
+        underTest.startObservingHost()
+        runCurrent()
+
+        val observer =
+            withArgCaptor<CommunalAppWidgetHost.Observer> {
+                verify(appWidgetHost).addObserver(capture())
+            }
+        observer.onHostStartListening()
+        return observer
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 2b3f40f..e3dd9ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -170,7 +170,7 @@
     @Test
     public void testBurnInProtectionStopsWhenContentViewDetached() {
         mController.onViewDetached();
-        verify(mHandler).removeCallbacks(any(Runnable.class));
+        verify(mHandler).removeCallbacksAndMessages(null);
     }
 
     @Test
@@ -281,4 +281,16 @@
 
         verify(mAnimationsController).cancelAnimations();
     }
+
+    @Test
+    public void onViewAttached_addsScrimExpansionCallback() {
+        mController.onViewAttached();
+        verify(mBouncerlessScrimController).addCallback(any());
+    }
+
+    @Test
+    public void onViewDetached_removesScrimExpansionCallback() {
+        mController.onViewDetached();
+        verify(mBouncerlessScrimController).removeCallback(any());
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
index 3b6f6a2..f31eb7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
@@ -58,7 +58,7 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class UdfpsKeyguardInteractorTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
     val keyguardRepository = kosmos.fakeKeyguardRepository
@@ -88,7 +88,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
index f52c66e..cde703b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -43,7 +43,7 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class AodToLockscreenTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class AodToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
     val repository = kosmos.fakeKeyguardTransitionRepository
@@ -60,7 +60,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index fee18dd..d632936 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -16,13 +16,15 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
 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
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -31,29 +33,25 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class BouncerToGoneFlowsTest : SysuiTestCase() {
-    @Mock private lateinit var shadeInteractor: ShadeInteractor
-
-    private val shadeExpansionStateFlow = MutableStateFlow(0.1f)
+@RunWith(ParameterizedAndroidJunit4::class)
+class BouncerToGoneFlowsTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     private val kosmos =
         testKosmos().apply {
@@ -61,16 +59,31 @@
         }
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
-    private val shadeRepository = kosmos.shadeRepository
     private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
     private val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
-    private val underTest = kosmos.bouncerToGoneFlows
+
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+    private lateinit var underTest: BouncerToGoneFlows
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(false)
         sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(false)
+        underTest = kosmos.bouncerToGoneFlows
     }
 
     @Test
@@ -79,7 +92,7 @@
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
             runCurrent()
 
-            shadeRepository.setLockscreenShadeExpansion(1f)
+            shadeTestUtil.setLockscreenShadeExpansion(1f)
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
 
             keyguardTransitionRepository.sendTransitionSteps(
@@ -99,12 +112,13 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun scrimAlpha_runDimissFromKeyguard_shadeNotExpanded() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
             runCurrent()
 
-            shadeRepository.setLockscreenShadeExpansion(0f)
+            shadeTestUtil.setLockscreenShadeExpansion(0f)
 
             whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
 
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 2e1765a..838b2a7 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
@@ -60,7 +60,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardRootViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
@@ -75,7 +75,6 @@
 
     private val viewState = ViewStateAccessor()
 
-    // add to init block
     companion object {
         @JvmStatic
         @Parameters(name = "{0}")
@@ -85,7 +84,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index ec2cb04..de4b999 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -47,7 +47,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenContentViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     private val kosmos: Kosmos = testKosmos()
 
@@ -62,7 +62,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
index e3ae3ba..bc381f2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
@@ -49,7 +49,7 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToAodTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenToAodTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
@@ -73,7 +73,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index adeb395..9337793 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -51,7 +51,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToDreamingTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenToDreamingTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     private val kosmos =
         testKosmos().apply {
@@ -73,7 +73,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index f8da74f..6ce7e88 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -54,7 +54,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -76,7 +76,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
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 d5df159..58c6817 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
@@ -46,7 +46,7 @@
 @ExperimentalCoroutinesApi
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterization?) :
+class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterization) :
     SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
@@ -67,7 +67,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
new file mode 100644
index 0000000..1e5599b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.ComponentName
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class IconAndNameCustomRepositoryTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    private val packageManager: PackageManager = kosmos.packageManager
+    private val userTracker: FakeUserTracker =
+        kosmos.fakeUserTracker.apply {
+            whenever(userContext.packageManager).thenReturn(packageManager)
+        }
+
+    private val service1 =
+        FakeInstalledTilesComponentRepository.ServiceInfo(
+            component1,
+            tileService1,
+            drawable1,
+            appName1,
+        )
+
+    private val service2 =
+        FakeInstalledTilesComponentRepository.ServiceInfo(
+            component2,
+            tileService2,
+            drawable2,
+            appName2,
+        )
+
+    private val underTest =
+        with(kosmos) {
+            IconAndNameCustomRepository(
+                installedTilesRepository,
+                userTracker,
+                mainCoroutineContext,
+            )
+        }
+
+    @Before
+    fun setUp() {
+        kosmos.fakeInstalledTilesRepository.setInstalledServicesForUser(
+            userTracker.userId,
+            listOf(service1, service2)
+        )
+    }
+
+    @Test
+    fun loadDataForCurrentServices() =
+        with(kosmos) {
+            testScope.runTest {
+                val editTileDataList = underTest.getCustomTileData()
+                val expectedData1 =
+                    EditTileData(
+                        TileSpec.create(component1),
+                        Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
+                        Text.Loaded(tileService1),
+                        Text.Loaded(appName1),
+                    )
+                val expectedData2 =
+                    EditTileData(
+                        TileSpec.create(component2),
+                        Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)),
+                        Text.Loaded(tileService2),
+                        Text.Loaded(appName2),
+                    )
+
+                assertThat(editTileDataList).containsExactly(expectedData1, expectedData2)
+            }
+        }
+
+    @Test
+    fun loadDataForCurrentServices_otherCurrentUser_empty() =
+        with(kosmos) {
+            testScope.runTest {
+                userTracker.set(listOf(UserInfo(11, "", 0)), 0)
+                val editTileDataList = underTest.getCustomTileData()
+
+                assertThat(editTileDataList).isEmpty()
+            }
+        }
+
+    @Test
+    fun loadDataForCurrentServices_serviceInfoWithNullIcon_notInList() =
+        with(kosmos) {
+            testScope.runTest {
+                val serviceNullIcon =
+                    FakeInstalledTilesComponentRepository.ServiceInfo(
+                        component2,
+                        tileService2,
+                    )
+                fakeInstalledTilesRepository.setInstalledServicesForUser(
+                    userTracker.userId,
+                    listOf(service1, serviceNullIcon)
+                )
+
+                val expectedData1 =
+                    EditTileData(
+                        TileSpec.create(component1),
+                        Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
+                        Text.Loaded(tileService1),
+                        Text.Loaded(appName1),
+                    )
+
+                val editTileDataList = underTest.getCustomTileData()
+                assertThat(editTileDataList).containsExactly(expectedData1)
+            }
+        }
+
+    private companion object {
+        val drawable1 = TestStubDrawable("drawable1")
+        val appName1 = "App1"
+        val tileService1 = "Tile Service 1"
+        val component1 = ComponentName("pkg1", "srv1")
+
+        val drawable2 = TestStubDrawable("drawable2")
+        val appName2 = "App2"
+        val tileService2 = "Tile Service 2"
+        val component2 = ComponentName("pkg2", "srv2")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt
new file mode 100644
index 0000000..56cead1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StockTilesRepositoryTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    private val underTest = StockTilesRepository(kosmos.mainResources)
+
+    @Test
+    fun stockTilesMatchesResources() {
+        val expected =
+            kosmos.mainResources
+                .getString(R.string.quick_settings_tiles_stock)
+                .split(",")
+                .map(TileSpec::create)
+        assertThat(underTest.stockTiles).isEqualTo(expected)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
new file mode 100644
index 0000000..deefbf5
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import android.content.ComponentName
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EditTilesListInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    // Only have some configurations so we can test the effect of missing configurations.
+    // As the configurations are injected by dagger, we'll have all the existing configurations
+    private val internetTileConfig = kosmos.qsInternetTileConfig
+    private val flashlightTileConfig = kosmos.qsFlashlightTileConfig
+    private val batteryTileConfig = kosmos.qsBatterySaverTileConfig
+
+    private val serviceInfo =
+        FakeInstalledTilesComponentRepository.ServiceInfo(
+            component,
+            tileName,
+            icon,
+            appName,
+        )
+
+    private val underTest =
+        with(kosmos) {
+            EditTilesListInteractor(
+                stockTilesRepository,
+                qSTileConfigProvider,
+                iconAndNameCustomRepository,
+            )
+        }
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            fakeInstalledTilesRepository.setInstalledServicesForUser(
+                userTracker.userId,
+                listOf(serviceInfo)
+            )
+
+            with(fakeQSTileConfigProvider) {
+                putConfig(internetTileConfig.tileSpec, internetTileConfig)
+                putConfig(flashlightTileConfig.tileSpec, flashlightTileConfig)
+                putConfig(batteryTileConfig.tileSpec, batteryTileConfig)
+            }
+        }
+    }
+
+    @Test
+    fun getTilesToEdit_stockTilesHaveNoAppName() =
+        with(kosmos) {
+            testScope.runTest {
+                val editTiles = underTest.getTilesToEdit()
+
+                assertThat(editTiles.stockTiles.all { it.appName == null }).isTrue()
+            }
+        }
+
+    @Test
+    fun getTilesToEdit_stockTilesAreAllPlatformSpecs() =
+        with(kosmos) {
+            testScope.runTest {
+                val editTiles = underTest.getTilesToEdit()
+
+                assertThat(editTiles.stockTiles.all { it.tileSpec is TileSpec.PlatformTileSpec })
+                    .isTrue()
+            }
+        }
+
+    @Test
+    fun getTilesToEdit_stockTiles_sameOrderAsRepository() =
+        with(kosmos) {
+            testScope.runTest {
+                val editTiles = underTest.getTilesToEdit()
+
+                assertThat(editTiles.stockTiles.map { it.tileSpec })
+                    .isEqualTo(stockTilesRepository.stockTiles)
+            }
+        }
+
+    @Test
+    fun getTilesToEdit_customTileData_matchesService() =
+        with(kosmos) {
+            testScope.runTest {
+                val editTiles = underTest.getTilesToEdit()
+                val expected =
+                    EditTileData(
+                        tileSpec = TileSpec.create(component),
+                        icon = Icon.Loaded(icon, ContentDescription.Loaded(tileName)),
+                        label = Text.Loaded(tileName),
+                        appName = Text.Loaded(appName),
+                    )
+
+                assertThat(editTiles.customTiles).hasSize(1)
+                assertThat(editTiles.customTiles[0]).isEqualTo(expected)
+            }
+        }
+
+    @Test
+    fun getTilesToEdit_tilesInConfigProvider_correctData() =
+        with(kosmos) {
+            testScope.runTest {
+                val editTiles = underTest.getTilesToEdit()
+
+                assertThat(
+                        editTiles.stockTiles.first { it.tileSpec == internetTileConfig.tileSpec }
+                    )
+                    .isEqualTo(internetTileConfig.toEditTileData())
+                assertThat(
+                        editTiles.stockTiles.first { it.tileSpec == flashlightTileConfig.tileSpec }
+                    )
+                    .isEqualTo(flashlightTileConfig.toEditTileData())
+                assertThat(editTiles.stockTiles.first { it.tileSpec == batteryTileConfig.tileSpec })
+                    .isEqualTo(batteryTileConfig.toEditTileData())
+            }
+        }
+
+    @Test
+    fun getTilesToEdit_tilesNotInConfigProvider_useDefaultData() =
+        with(kosmos) {
+            testScope.runTest {
+                underTest
+                    .getTilesToEdit()
+                    .stockTiles
+                    .filterNot { qSTileConfigProvider.hasConfig(it.tileSpec.spec) }
+                    .forEach { assertThat(it).isEqualTo(it.tileSpec.missingConfigEditTileData()) }
+            }
+        }
+
+    private companion object {
+        val component = ComponentName("pkg", "srv")
+        const val tileName = "Tile Service"
+        const val appName = "App"
+        val icon = TestStubDrawable("icon")
+
+        fun TileSpec.missingConfigEditTileData(): EditTileData {
+            return EditTileData(
+                tileSpec = this,
+                icon = Icon.Resource(android.R.drawable.star_on, ContentDescription.Loaded(spec)),
+                label = Text.Loaded(spec),
+                appName = null
+            )
+        }
+
+        fun QSTileConfig.toEditTileData(): EditTileData {
+            return EditTileData(
+                tileSpec = tileSpec,
+                icon =
+                    Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
+                label = Text.Resource(uiConfig.labelRes),
+                appName = null,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
new file mode 100644
index 0000000..9fb25a28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import android.R
+import android.content.ComponentName
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.qsTileFactory
+import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.impl.sensorprivacy.qsCameraSensorPrivacyToggleTileConfig
+import com.android.systemui.qs.tiles.impl.sensorprivacy.qsMicrophoneSensorPrivacyToggleTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EditModeViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
+    // Only have some configurations so we can test the effect of missing configurations.
+    // As the configurations are injected by dagger, we'll have all the existing configurations
+    private val configs =
+        with(kosmos) {
+            setOf(
+                qsInternetTileConfig,
+                qsFlashlightTileConfig,
+                qsBatterySaverTileConfig,
+                qsAlarmTileConfig,
+                qsCameraSensorPrivacyToggleTileConfig,
+                qsMicrophoneSensorPrivacyToggleTileConfig,
+            )
+        }
+
+    private val serviceInfo1 =
+        FakeInstalledTilesComponentRepository.ServiceInfo(
+            component1,
+            tileService1,
+            drawable1,
+            appName1,
+        )
+
+    private val serviceInfo2 =
+        FakeInstalledTilesComponentRepository.ServiceInfo(
+            component2,
+            tileService2,
+            drawable2,
+            appName2,
+        )
+
+    private val underTest: EditModeViewModel by lazy {
+        with(kosmos) {
+            EditModeViewModel(
+                editTilesListInteractor,
+                currentTilesInteractor,
+                minimumTilesInteractor,
+                infiniteGridLayout,
+                applicationCoroutineScope,
+                gridLayoutTypeInteractor,
+                gridLayoutMap,
+            )
+        }
+    }
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            fakeMinimumTilesRepository = MinimumTilesFixedRepository(minNumberOfTiles)
+
+            fakeInstalledTilesRepository.setInstalledServicesForUser(
+                userTracker.userId,
+                listOf(serviceInfo1, serviceInfo2)
+            )
+
+            with(fakeQSTileConfigProvider) { configs.forEach { putConfig(it.tileSpec, it) } }
+            qsTileFactory = FakeQSFactory { FakeQSTile(userTracker.userId, available = true) }
+        }
+    }
+
+    @Test
+    fun isEditing() =
+        with(kosmos) {
+            testScope.runTest {
+                val isEditing by collectLastValue(underTest.isEditing)
+
+                assertThat(isEditing).isFalse()
+
+                underTest.startEditing()
+                assertThat(isEditing).isTrue()
+
+                underTest.stopEditing()
+                assertThat(isEditing).isFalse()
+            }
+        }
+
+    @Test
+    fun editing_false_emptyFlowOfTiles() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+
+                assertThat(tiles).isNull()
+            }
+        }
+
+    @Test
+    fun editing_true_notEmptyTileData() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+
+                underTest.startEditing()
+
+                assertThat(tiles).isNotEmpty()
+            }
+        }
+
+    @Test
+    fun tilesData_hasAllStockTiles() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+
+                underTest.startEditing()
+
+                assertThat(
+                        tiles!!
+                            .filter { it.tileSpec is TileSpec.PlatformTileSpec }
+                            .map { it.tileSpec }
+                    )
+                    .containsExactlyElementsIn(stockTilesRepository.stockTiles)
+            }
+        }
+
+    @Test
+    fun tilesData_stockTiles_haveCorrectUiValues() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+
+                underTest.startEditing()
+
+                tiles!!
+                    .filter { it.tileSpec is TileSpec.PlatformTileSpec }
+                    .forEach {
+                        val data = getEditTileData(it.tileSpec)
+
+                        assertThat(it.label).isEqualTo(data.label)
+                        assertThat(it.icon).isEqualTo(data.icon)
+                        assertThat(it.appName).isNull()
+                    }
+            }
+        }
+
+    @Test
+    fun tilesData_hasAllCustomTiles() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+
+                underTest.startEditing()
+
+                assertThat(
+                        tiles!!
+                            .filter { it.tileSpec is TileSpec.CustomTileSpec }
+                            .map { it.tileSpec }
+                    )
+                    .containsExactly(TileSpec.create(component1), TileSpec.create(component2))
+            }
+        }
+
+    @Test
+    fun tilesData_customTiles_haveCorrectUiValues() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+
+                underTest.startEditing()
+
+                // service1
+                val model1 = tiles!!.first { it.tileSpec == TileSpec.create(component1) }
+                assertThat(model1.label).isEqualTo(Text.Loaded(tileService1))
+                assertThat(model1.appName).isEqualTo(Text.Loaded(appName1))
+                assertThat(model1.icon)
+                    .isEqualTo(Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)))
+
+                // service2
+                val model2 = tiles!!.first { it.tileSpec == TileSpec.create(component2) }
+                assertThat(model2.label).isEqualTo(Text.Loaded(tileService2))
+                assertThat(model2.appName).isEqualTo(Text.Loaded(appName2))
+                assertThat(model2.icon)
+                    .isEqualTo(Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)))
+            }
+        }
+
+    @Test
+    fun currentTiles_inCorrectOrder_markedAsCurrent() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    listOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+
+                underTest.startEditing()
+
+                assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec })
+                    .containsExactlyElementsIn(currentTiles)
+                    .inOrder()
+            }
+        }
+
+    @Test
+    fun notCurrentTiles() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    listOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                val remainingTiles =
+                    stockTilesRepository.stockTiles.filterNot { it in currentTiles } +
+                        listOf(TileSpec.create(component1))
+                currentTilesInteractor.setTiles(currentTiles)
+
+                underTest.startEditing()
+
+                assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec })
+                    .containsExactlyElementsIn(remainingTiles)
+            }
+        }
+
+    @Test
+    fun currentTilesChange_trackingChange() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+
+                underTest.startEditing()
+
+                val newTile = TileSpec.create("internet")
+                val position = 1
+                currentTilesInteractor.addTile(newTile, position)
+                currentTiles.add(position, newTile)
+
+                assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec })
+                    .containsExactlyElementsIn(currentTiles)
+                    .inOrder()
+            }
+        }
+
+    @Test
+    fun nonCurrentTiles_orderPreservedWhenCurrentTilesChange() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+
+                underTest.startEditing()
+
+                val nonCurrentSpecs = tiles!!.filterNot { it.isCurrent }.map { it.tileSpec }
+                val newTile = TileSpec.create("internet")
+                currentTilesInteractor.addTile(newTile)
+
+                assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec })
+                    .containsExactlyElementsIn(nonCurrentSpecs - listOf(newTile))
+                    .inOrder()
+            }
+        }
+
+    @Test
+    fun nonCurrentTiles_haveOnlyAddAction() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+
+                underTest.startEditing()
+
+                tiles!!
+                    .filterNot { it.isCurrent }
+                    .forEach {
+                        assertThat(it.availableEditActions)
+                            .containsExactly(AvailableEditActions.ADD)
+                    }
+            }
+        }
+
+    @Test
+    fun currentTiles_moreThanMinimumTiles_haveRemoveAction() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+                assertThat(currentTiles.size).isGreaterThan(minNumberOfTiles)
+
+                underTest.startEditing()
+
+                tiles!!
+                    .filter { it.isCurrent }
+                    .forEach {
+                        assertThat(it.availableEditActions).contains(AvailableEditActions.REMOVE)
+                    }
+            }
+        }
+
+    @Test
+    fun currentTiles_minimumTiles_dontHaveRemoveAction() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+                assertThat(currentTiles.size).isEqualTo(minNumberOfTiles)
+
+                underTest.startEditing()
+
+                tiles!!
+                    .filter { it.isCurrent }
+                    .forEach {
+                        assertThat(it.availableEditActions)
+                            .doesNotContain(AvailableEditActions.REMOVE)
+                    }
+            }
+        }
+
+    @Test
+    fun currentTiles_lessThanMinimumTiles_dontHaveRemoveAction() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+                assertThat(currentTiles.size).isLessThan(minNumberOfTiles)
+
+                underTest.startEditing()
+
+                tiles!!
+                    .filter { it.isCurrent }
+                    .forEach {
+                        assertThat(it.availableEditActions)
+                            .doesNotContain(AvailableEditActions.REMOVE)
+                    }
+            }
+        }
+
+    @Test
+    fun currentTiles_haveMoveAction() =
+        with(kosmos) {
+            testScope.runTest {
+                val tiles by collectLastValue(underTest.tiles)
+                val currentTiles =
+                    mutableListOf(
+                        TileSpec.create("flashlight"),
+                        TileSpec.create("airplane"),
+                        TileSpec.create(component2),
+                        TileSpec.create("alarm"),
+                    )
+                currentTilesInteractor.setTiles(currentTiles)
+
+                underTest.startEditing()
+
+                tiles!!
+                    .filter { it.isCurrent }
+                    .forEach {
+                        assertThat(it.availableEditActions).contains(AvailableEditActions.MOVE)
+                    }
+            }
+        }
+
+    private companion object {
+        val drawable1 = TestStubDrawable("drawable1")
+        val appName1 = "App1"
+        val tileService1 = "Tile Service 1"
+        val component1 = ComponentName("pkg1", "srv1")
+
+        val drawable2 = TestStubDrawable("drawable2")
+        val appName2 = "App2"
+        val tileService2 = "Tile Service 2"
+        val component2 = ComponentName("pkg2", "srv2")
+
+        fun TileSpec.missingConfigEditTileData(): EditTileData {
+            return EditTileData(
+                tileSpec = this,
+                icon = Icon.Resource(R.drawable.star_on, ContentDescription.Loaded(spec)),
+                label = Text.Loaded(spec),
+                appName = null
+            )
+        }
+
+        fun QSTileConfig.toEditTileData(): EditTileData {
+            return EditTileData(
+                tileSpec = tileSpec,
+                icon =
+                    Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
+                label = Text.Resource(uiConfig.labelRes),
+                appName = null,
+            )
+        }
+
+        fun Kosmos.getEditTileData(tileSpec: TileSpec): EditTileData {
+            return if (qSTileConfigProvider.hasConfig(tileSpec.spec)) {
+                qSTileConfigProvider.getConfig(tileSpec.spec).toEditTileData()
+            } else {
+                tileSpec.missingConfigEditTileData()
+            }
+        }
+
+        val minNumberOfTiles = 3
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index bc57ce6..a0dec8c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -90,7 +90,7 @@
     }
 
     @Test
-    fun componentsLoadedOnStart() =
+    fun servicesLoadedOnStart() =
         testScope.runTest {
             val userId = 0
             val resolveInfo =
@@ -106,12 +106,14 @@
 
             val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
             runCurrent()
+            val services = underTest.getInstalledTilesServiceInfos(userId)
 
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
+            assertThat(services).containsExactly(resolveInfo.serviceInfo)
         }
 
     @Test
-    fun componentAdded_foundAfterPackageChange() =
+    fun serviceAdded_foundAfterPackageChange() =
         testScope.runTest {
             val userId = 0
             val resolveInfo =
@@ -132,12 +134,14 @@
                 .thenReturn(listOf(resolveInfo))
             kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
             runCurrent()
+            val services = underTest.getInstalledTilesServiceInfos(userId)
 
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
+            assertThat(services).containsExactly(resolveInfo.serviceInfo)
         }
 
     @Test
-    fun componentWithoutPermission_notValid() =
+    fun serviceWithoutPermission_notValid() =
         testScope.runTest {
             val userId = 0
             val resolveInfo =
@@ -152,13 +156,15 @@
                 .thenReturn(listOf(resolveInfo))
 
             val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+            val services = underTest.getInstalledTilesServiceInfos(userId)
             runCurrent()
 
             assertThat(componentNames).isEmpty()
+            assertThat(services).isEmpty()
         }
 
     @Test
-    fun componentNotEnabled_notValid() =
+    fun serviceNotEnabled_notValid() =
         testScope.runTest {
             val userId = 0
             val resolveInfo =
@@ -173,9 +179,11 @@
                 .thenReturn(listOf(resolveInfo))
 
             val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+            val services = underTest.getInstalledTilesServiceInfos(userId)
             runCurrent()
 
             assertThat(componentNames).isEmpty()
+            assertThat(services).isEmpty()
         }
 
     @Test
@@ -221,30 +229,22 @@
 
             val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
             runCurrent()
+            val service = underTest.getInstalledTilesServiceInfos(userId)
 
             assertThat(componentNames).containsExactly(TEST_COMPONENT)
+            assertThat(service).containsExactly(resolveInfo.serviceInfo)
         }
 
     @Test
-    fun loadComponentsForSameUserTwice_returnsSameFlow() =
+    fun loadServicesForSameUserTwice_returnsSameFlow() =
         testScope.runTest {
-            val flowForUser1 = underTest.getInstalledTilesComponents(1)
-            val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1)
+            val flowForUser1 = underTest.getInstalledTilesServiceInfos(1)
+            val flowForUser1TheSecondTime = underTest.getInstalledTilesServiceInfos(1)
             runCurrent()
 
             assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1)
         }
 
-    @Test
-    fun loadComponentsForDifferentUsers_returnsDifferentFlow() =
-        testScope.runTest {
-            val flowForUser1 = underTest.getInstalledTilesComponents(1)
-            val flowForUser2 = underTest.getInstalledTilesComponents(2)
-            runCurrent()
-
-            assertThat(flowForUser2).isNotEqualTo(flowForUser1)
-        }
-
     // Tests that a ServiceInfo that is returned by queryIntentServicesAsUser but shortly
     // after uninstalled, doesn't crash SystemUI.
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
index 61e4774..3faab50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository
 import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
 import com.android.systemui.qs.pipeline.domain.model.TileModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tiles.ColorCorrectionTile
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
index 8ae9172..167eff1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.FakeQSTile
 import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
 import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable
 import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 634c5fa..1c73fe2b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.qs.QSTile.BooleanState
 import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
 import com.android.systemui.qs.external.CustomTile
 import com.android.systemui.qs.external.CustomTileStatePersister
 import com.android.systemui.qs.external.TileLifecycleManager
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
index 90c8304..260189d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
 import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.data.repository.FakeDefaultTilesRepository
 import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
index 4207a9c..dffd0d7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
 import com.android.systemui.qs.pipeline.data.model.RestoreData
 import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository
 import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository
@@ -54,9 +55,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() {
 
-    private val kosmos by lazy {
-        Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) }
-    }
+    private val kosmos by lazy { Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } }
     // Getter here so it can change when there is a managed profile.
     private val workTileAvailable: Boolean
         get() = hasManagedProfile()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
new file mode 100644
index 0000000..a0aa2d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.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.qs.tiles.impl.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.DateFormatUtil
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileDataInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val testUser = UserHandle.of(1)!!
+    private val testStartTime = LocalTime.MIDNIGHT
+    private val testEndTime = LocalTime.NOON
+    private val colorDisplayManager =
+        mock<ColorDisplayManager> {
+            whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+            whenever(isNightDisplayActivated).thenReturn(false)
+            whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+            whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+        }
+    private val locationController = FakeLocationController(LeakCheck())
+    private val nightDisplayListener = mock<NightDisplayListener>()
+    private val listenerBuilder =
+        mock<NightDisplayListenerModule.Builder> {
+            whenever(setUser(anyInt())).thenReturn(this)
+            whenever(build()).thenReturn(nightDisplayListener)
+        }
+    private val globalSettings = kosmos.fakeGlobalSettings
+    private val secureSettings = kosmos.fakeSettings
+    private val dateFormatUtil = mock<DateFormatUtil> { whenever(is24HourFormat).thenReturn(false) }
+    private val testDispatcher = StandardTestDispatcher()
+    private val scope = TestScope(testDispatcher)
+    private val userScopedColorDisplayManager =
+        mock<UserScopedService<ColorDisplayManager>> {
+            whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+        }
+    private val nightDisplayRepository =
+        NightDisplayRepository(
+            testDispatcher,
+            scope.backgroundScope,
+            globalSettings,
+            secureSettings,
+            listenerBuilder,
+            userScopedColorDisplayManager,
+            locationController,
+        )
+
+    private val underTest: NightDisplayTileDataInteractor =
+        NightDisplayTileDataInteractor(context, dateFormatUtil, nightDisplayRepository)
+
+    @Test
+    fun availability_matchesColorDisplayManager() = runTest {
+        val availability by collectLastValue(underTest.availability(testUser))
+
+        val expectedAvailability = ColorDisplayManager.isNightDisplayAvailable(context)
+        assertThat(availability).isEqualTo(expectedAvailability)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..adc8bcb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.intentInputs
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.custom.qsTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileUserActionInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()
+    private val testUser = UserHandle.of(1)
+    private val colorDisplayManager =
+        mock<ColorDisplayManager> {
+            whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+            whenever(isNightDisplayActivated).thenReturn(false)
+        }
+    private val locationController = FakeLocationController(LeakCheck())
+    private val nightDisplayListener = mock<NightDisplayListener>()
+    private val listenerBuilder =
+        mock<NightDisplayListenerModule.Builder> {
+            whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+            whenever(build()).thenReturn(nightDisplayListener)
+        }
+    private val globalSettings = kosmos.fakeGlobalSettings
+    private val secureSettings = kosmos.fakeSettings
+    private val testDispatcher = StandardTestDispatcher()
+    private val scope = TestScope(testDispatcher)
+    private val userScopedColorDisplayManager =
+        mock<UserScopedService<ColorDisplayManager>> {
+            whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+        }
+    private val nightDisplayRepository =
+        NightDisplayRepository(
+            testDispatcher,
+            scope.backgroundScope,
+            globalSettings,
+            secureSettings,
+            listenerBuilder,
+            userScopedColorDisplayManager,
+            locationController,
+        )
+
+    private val underTest =
+        NightDisplayTileUserActionInteractor(
+            nightDisplayRepository,
+            qsTileIntentUserActionHandler,
+            kosmos.qsTileLogger
+        )
+
+    @Test
+    fun handleClick_inactive_activates() =
+        scope.runTest {
+            val startingModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+            underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+            verify(colorDisplayManager).setNightDisplayActivated(true)
+        }
+
+    @Test
+    fun handleClick_active_disables() =
+        scope.runTest {
+            val startingModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+            underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+            verify(colorDisplayManager).setNightDisplayActivated(false)
+        }
+
+    @Test
+    fun handleClick_whenAutoModeTwilight_flipsState() =
+        scope.runTest {
+            val originalState = true
+            val startingModel = NightDisplayTileModel.AutoModeTwilight(originalState, false, false)
+
+            underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+            verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+        }
+
+    @Test
+    fun handleClick_whenAutoModeCustom_flipsState() =
+        scope.runTest {
+            val originalState = true
+            val startingModel =
+                NightDisplayTileModel.AutoModeCustom(originalState, false, null, null, false)
+
+            underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+            verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+        }
+
+    @Test
+    fun handleLongClickWhenEnabled() =
+        scope.runTest {
+            val enabledState = true
+
+            underTest.handleInput(
+                QSTileInputTestKtx.longClick(
+                    NightDisplayTileModel.AutoModeOff(enabledState, false),
+                    testUser
+                )
+            )
+
+            assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+            val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+            val actualIntentAction = intentInput.intent.action
+            val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+            assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+        }
+
+    @Test
+    fun handleLongClickWhenDisabled() =
+        scope.runTest {
+            val enabledState = false
+
+            underTest.handleInput(
+                QSTileInputTestKtx.longClick(
+                    NightDisplayTileModel.AutoModeOff(enabledState, false),
+                    testUser
+                )
+            )
+
+            assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+            val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+            val actualIntentAction = intentInput.intent.action
+            val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+            assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
new file mode 100644
index 0000000..5d2e701
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.qsNightDisplayTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.mock
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileMapperTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val config = kosmos.qsNightDisplayTileConfig
+
+    private val testStartTime = LocalTime.MIDNIGHT
+    private val testEndTime = LocalTime.NOON
+
+    private lateinit var mapper: NightDisplayTileMapper
+
+    @Before
+    fun setup() {
+        mapper =
+            NightDisplayTileMapper(
+                context.orCreateTestableResources
+                    .apply {
+                        addOverride(R.drawable.qs_nightlight_icon_on, TestStubDrawable())
+                        addOverride(R.drawable.qs_nightlight_icon_off, TestStubDrawable())
+                    }
+                    .resources,
+                context.theme,
+                mock<QSTileLogger>(),
+            )
+    }
+
+    @Test
+    fun disabledModel_whenAutoModeOff() {
+        val inputModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    /** Force enable does not change the mode by itself. */
+    @Test
+    fun disabledModel_whenAutoModeOff_whenForceEnable() {
+        val inputModel = NightDisplayTileModel.AutoModeOff(false, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel_whenAutoModeOff() {
+        val inputModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel_forceAutoMode_whenAutoModeOff() {
+        val inputModel = NightDisplayTileModel.AutoModeOff(true, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel_autoModeTwilight_locationOff() {
+        val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState = createNightDisplayTileState(QSTileState.ActivationState.ACTIVE, null)
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel_autoModeTwilight_locationOn() {
+        val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(R.string.quick_settings_night_secondary_label_until_sunrise)
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun disabledModel_autoModeTwilight_locationOn() {
+        val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.getString(R.string.quick_settings_night_secondary_label_on_at_sunset)
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun disabledModel_autoModeTwilight_locationOff() {
+        val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState = createNightDisplayTileState(QSTileState.ActivationState.INACTIVE, null)
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun disabledModel_autoModeCustom_24Hour() {
+        val inputModel =
+            NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.getString(
+                    R.string.quick_settings_night_secondary_label_on_at,
+                    formatter24Hour.format(testStartTime)
+                )
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun disabledModel_autoModeCustom_12Hour() {
+        val inputModel =
+            NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.getString(
+                    R.string.quick_settings_night_secondary_label_on_at,
+                    formatter12Hour.format(testStartTime)
+                )
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    /** Should have the same outcome as [disabledModel_autoModeCustom_12Hour] */
+    @Test
+    fun disabledModel_autoModeCustom_12Hour_isEnrolledForcedAutoMode() {
+        val inputModel =
+            NightDisplayTileModel.AutoModeCustom(false, true, testStartTime, null, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.INACTIVE,
+                context.getString(
+                    R.string.quick_settings_night_secondary_label_on_at,
+                    formatter12Hour.format(testStartTime)
+                )
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel_autoModeCustom_24Hour() {
+        val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(
+                    R.string.quick_settings_secondary_label_until,
+                    formatter24Hour.format(testEndTime)
+                )
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    @Test
+    fun enabledModel_autoModeCustom_12Hour() {
+        val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, false)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(
+                    R.string.quick_settings_secondary_label_until,
+                    formatter12Hour.format(testEndTime)
+                )
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    /** Should have the same state as [enabledModel_autoModeCustom_24Hour] */
+    @Test
+    fun enabledModel_autoModeCustom_24Hour_forceEnabled() {
+        val inputModel = NightDisplayTileModel.AutoModeCustom(true, true, null, testEndTime, true)
+
+        val outputState = mapper.map(config, inputModel)
+
+        val expectedState =
+            createNightDisplayTileState(
+                QSTileState.ActivationState.ACTIVE,
+                context.getString(
+                    R.string.quick_settings_secondary_label_until,
+                    formatter24Hour.format(testEndTime)
+                )
+            )
+        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+    }
+
+    private fun createNightDisplayTileState(
+        activationState: QSTileState.ActivationState,
+        secondaryLabel: String?
+    ): QSTileState {
+        val label = context.getString(R.string.quick_settings_night_display_label)
+
+        val contentDescription =
+            if (TextUtils.isEmpty(secondaryLabel)) label
+            else TextUtils.concat(label, ", ", secondaryLabel)
+        return QSTileState(
+            {
+                Icon.Loaded(
+                    context.getDrawable(
+                        if (activationState == QSTileState.ActivationState.ACTIVE)
+                            R.drawable.qs_nightlight_icon_on
+                        else R.drawable.qs_nightlight_icon_off
+                    )!!,
+                    null
+                )
+            },
+            label,
+            activationState,
+            secondaryLabel,
+            setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+            contentDescription,
+            null,
+            QSTileState.SideViewIcon.None,
+            QSTileState.EnabledState.ENABLED,
+            Switch::class.qualifiedName
+        )
+    }
+
+    private companion object {
+        val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+        val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt
new file mode 100644
index 0000000..a2ffe70
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class GoneSceneViewModelTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val shadeRepository by lazy { kosmos.shadeRepository }
+    private lateinit var underTest: GoneSceneViewModel
+
+    @Before
+    fun setUp() {
+        underTest =
+            GoneSceneViewModel(
+                applicationScope = testScope.backgroundScope,
+                shadeInteractor = kosmos.shadeInteractor,
+            )
+    }
+
+    @Test
+    fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            runCurrent()
+
+            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey)
+                .isEqualTo(GoneToSplitShade)
+        }
+
+    @Test
+    fun downTransitionKey_splitShadeDisabled_isNull() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            shadeRepository.setShadeMode(ShadeMode.Single)
+            runCurrent()
+
+            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull()
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index aa0ca18..78c4def 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -60,7 +60,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class ShadeInteractorImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
     val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
@@ -85,7 +85,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index 44c9695..cecc70c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -60,7 +60,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val shadeInteractor by lazy { kosmos.shadeInteractor }
@@ -80,7 +80,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 5312ad8..2439217 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
 import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -159,6 +160,27 @@
         }
 
     @Test
+    fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            shadeRepository.setShadeMode(ShadeMode.Split)
+            runCurrent()
+
+            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey)
+                .isEqualTo(GoneToSplitShade)
+        }
+
+    @Test
+    fun upTransitionKey_splitShadeDisabled_isNull() =
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
+            shadeRepository.setShadeMode(ShadeMode.Single)
+            runCurrent()
+
+            assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull()
+        }
+
+    @Test
     fun isClickable_deviceUnlocked_false() =
         testScope.runTest {
             val isClickable by collectLastValue(underTest.isClickable)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index d353a62..f06e04b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -33,7 +33,10 @@
 import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -65,11 +68,11 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 @TestableLooper.RunWithLooper
-class StatusBarStateControllerImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
     private val mockDarkAnimator = mock<ObjectAnimator>()
 
     private lateinit var underTest: StatusBarStateControllerImpl
@@ -84,7 +87,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
@@ -98,6 +101,7 @@
                     uiEventLogger,
                     { kosmos.interactionJankMonitor },
                     JavaAdapter(testScope.backgroundScope),
+                    { kosmos.keyguardTransitionInteractor },
                     { kosmos.shadeInteractor },
                     { kosmos.deviceUnlockedInteractor },
                     { kosmos.sceneInteractor },
@@ -330,4 +334,25 @@
             assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
             assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
         }
+
+    @Test
+    fun leaveOpenOnKeyguard_whenGone_isFalse() =
+        testScope.runTest {
+            underTest.start()
+            underTest.setLeaveOpenOnKeyguardHide(true)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                testScope = testScope,
+            )
+            assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(true)
+
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GONE,
+                testScope = testScope,
+            )
+            assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(false)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index cbbc4d8..9367a93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -59,7 +59,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterization?) :
+class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterization) :
     SysuiTestCase() {
 
     companion object {
@@ -71,7 +71,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 7ac549a..cc5df74 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -20,12 +20,13 @@
 
 import android.app.NotificationManager.Policy
 import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
-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.flags.Flags
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -33,7 +34,7 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.res.R
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -56,11 +57,13 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @EnableFlags(FooterViewRefactor.FLAG_NAME)
-class NotificationListViewModelTest : SysuiTestCase() {
+class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -72,16 +75,30 @@
     private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
     private val fakePowerRepository = kosmos.fakePowerRepository
     private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
-    private val fakeShadeRepository = kosmos.fakeShadeRepository
     private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
     private val headsUpRepository = kosmos.headsUpNotificationRepository
     private val zenModeRepository = kosmos.zenModeRepository
 
-    val underTest = kosmos.notificationListViewModel
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+    private lateinit var underTest: NotificationListViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        underTest = kosmos.notificationListViewModel
     }
 
     @Test
@@ -163,7 +180,7 @@
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
             // AND quick settings are expanded
-            fakeShadeRepository.legacyQsFullscreen.value = true
+            shadeTestUtil.setQsFullscreen(true)
             runCurrent()
 
             // THEN empty shade is not visible
@@ -178,9 +195,10 @@
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
             // AND quick settings are expanded
-            fakeShadeRepository.setQsExpansion(1f)
-            // AND split shade is enabled
+            shadeTestUtil.setQsExpansion(1f)
+            // AND split shade is expanded
             overrideResource(R.bool.config_use_split_notification_shade, true)
+            shadeTestUtil.setShadeExpansion(1f)
             fakeConfigurationController.notifyConfigurationChanged()
             runCurrent()
 
@@ -290,7 +308,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             runCurrent()
 
             // THEN footer is visible
@@ -306,7 +324,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open on lockscreen
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             runCurrent()
 
             // THEN footer is visible
@@ -337,7 +355,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             // AND user is not set up
             fakeUserSetupRepository.setUserSetUp(false)
             runCurrent()
@@ -355,7 +373,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             // AND device is starting to go to sleep
             fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP)
             runCurrent()
@@ -373,10 +391,10 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             // AND quick settings are expanded
-            fakeShadeRepository.setQsExpansion(1f)
-            fakeShadeRepository.legacyQsFullscreen.value = true
+            shadeTestUtil.setQsExpansion(1f)
+            shadeTestUtil.setQsFullscreen(true)
             runCurrent()
 
             // THEN footer is not visible
@@ -390,11 +408,11 @@
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
+            // AND quick settings are expanded
+            shadeTestUtil.setQsExpansion(1f)
             // AND shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
-            // AND quick settings are expanded
-            fakeShadeRepository.setQsExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             // AND split shade is enabled
             overrideResource(R.bool.config_use_split_notification_shade, true)
             fakeConfigurationController.notifyConfigurationChanged()
@@ -413,7 +431,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             // AND remote input is active
             fakeRemoteInputRepository.isRemoteInputActive.value = true
             runCurrent()
@@ -431,7 +449,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND shade is open and fully expanded
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             runCurrent()
 
             // THEN footer visibility animates
@@ -447,7 +465,7 @@
             activeNotificationListRepository.setActiveNotifs(count = 2)
             // AND we are on the keyguard
             fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             runCurrent()
 
             // THEN footer visibility does not animate
@@ -461,7 +479,7 @@
 
             // WHEN shade is closed
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(0f)
+            shadeTestUtil.setShadeExpansion(0f)
             runCurrent()
 
             // THEN footer is hidden
@@ -475,7 +493,7 @@
 
             // WHEN shade is open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setShadeExpansion(1f)
             runCurrent()
 
             // THEN footer is hidden
@@ -489,8 +507,8 @@
 
             // WHEN QS partially open
             fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            fakeShadeRepository.setQsExpansion(0.5f)
-            fakeShadeRepository.setLegacyShadeExpansion(0.5f)
+            shadeTestUtil.setQsExpansion(0.5f)
+            shadeTestUtil.setShadeExpansion(0.5f)
             runCurrent()
 
             // THEN footer is hidden
@@ -588,7 +606,7 @@
         testScope.runTest {
             val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
 
-            fakeShadeRepository.setQsExpansion(0.0f)
+            shadeTestUtil.setQsExpansion(0.0f)
             fakeKeyguardRepository.setKeyguardShowing(false)
             runCurrent()
 
@@ -601,7 +619,7 @@
         testScope.runTest {
             val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
 
-            fakeShadeRepository.setQsExpansion(0.0f)
+            shadeTestUtil.setQsExpansion(0.0f)
             fakeKeyguardRepository.setKeyguardShowing(true)
             runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 2cd295c..f2ce745 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -75,7 +75,7 @@
 @RunWith(ParameterizedAndroidJunit4::class)
 // SharedNotificationContainerViewModel is only bound when FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT is on
 @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-class SharedNotificationContainerViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     companion object {
         @JvmStatic
@@ -89,7 +89,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     val aodBurnInViewModel = mock(AodBurnInViewModel::class.java)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index 8ce5037..63f19fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -249,22 +249,40 @@
 
 
     @Test
-    fun testDelete_showingEntryKeyBecomesPreviousHunKey() {
+    fun testDelete_deleteSecondToLastEntry_showingEntryKeyBecomesPreviousHunKey() {
         mAvalancheController.previousHunKey = ""
 
         // Entry is showing
+        val firstEntry = createHeadsUpEntry(id = 0)
+        mAvalancheController.headsUpEntryShowing = firstEntry
+
+        // There's another entry waiting to show next
+        val secondEntry = createHeadsUpEntry(id = 1)
+        mAvalancheController.addToNext(secondEntry, runnableMock!!)
+
+        // Delete
+        mAvalancheController.delete(firstEntry, runnableMock, "testLabel")
+
+        // Next entry is shown
+        assertThat(mAvalancheController.previousHunKey).isEqualTo(firstEntry.mEntry!!.key)
+    }
+
+    @Test
+    fun testDelete_deleteLastEntry_previousHunKeyCleared() {
+        mAvalancheController.previousHunKey = "key"
+
+        // Nothing waiting to show
+        mAvalancheController.clearNext()
+
+        // One entry is showing
         val showingEntry = createHeadsUpEntry(id = 0)
         mAvalancheController.headsUpEntryShowing = showingEntry
 
-        // There's another entry waiting to show next
-        val nextEntry = createHeadsUpEntry(id = 1)
-        mAvalancheController.addToNext(nextEntry, runnableMock!!)
-
         // Delete
-        mAvalancheController.delete(showingEntry, runnableMock, "testLabel")
+        mAvalancheController.delete(showingEntry, runnableMock!!, "testLabel")
 
         // Next entry is shown
-        assertThat(mAvalancheController.previousHunKey).isEqualTo(showingEntry.mEntry!!.key)
+        assertThat(mAvalancheController.previousHunKey).isEqualTo("");
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
index 03a39f8..2d8cd93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -23,9 +23,9 @@
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,21 +45,21 @@
     @Test
     fun and_allTrue_returnsTrue() =
         testScope.runTest {
-            val result by collectLastValue(and(TRUE, TRUE))
+            val result by collectLastValue(allOf(TRUE, TRUE))
             assertThat(result).isTrue()
         }
 
     @Test
     fun and_anyFalse_returnsFalse() =
         testScope.runTest {
-            val result by collectLastValue(and(TRUE, FALSE, TRUE))
+            val result by collectLastValue(allOf(TRUE, FALSE, TRUE))
             assertThat(result).isFalse()
         }
 
     @Test
     fun and_allFalse_returnsFalse() =
         testScope.runTest {
-            val result by collectLastValue(and(FALSE, FALSE, FALSE))
+            val result by collectLastValue(allOf(FALSE, FALSE, FALSE))
             assertThat(result).isFalse()
         }
 
@@ -68,7 +68,7 @@
         testScope.runTest {
             val flow1 = MutableStateFlow(false)
             val flow2 = MutableStateFlow(false)
-            val values by collectValues(and(flow1, flow2))
+            val values by collectValues(allOf(flow1, flow2))
 
             assertThat(values).containsExactly(false)
             flow1.value = true
@@ -81,21 +81,21 @@
     @Test
     fun or_allTrue_returnsTrue() =
         testScope.runTest {
-            val result by collectLastValue(or(TRUE, TRUE))
+            val result by collectLastValue(anyOf(TRUE, TRUE))
             assertThat(result).isTrue()
         }
 
     @Test
     fun or_anyTrue_returnsTrue() =
         testScope.runTest {
-            val result by collectLastValue(or(FALSE, TRUE, FALSE))
+            val result by collectLastValue(anyOf(FALSE, TRUE, FALSE))
             assertThat(result).isTrue()
         }
 
     @Test
     fun or_allFalse_returnsFalse() =
         testScope.runTest {
-            val result by collectLastValue(or(FALSE, FALSE, FALSE))
+            val result by collectLastValue(anyOf(FALSE, FALSE, FALSE))
             assertThat(result).isFalse()
         }
 
@@ -104,7 +104,7 @@
         testScope.runTest {
             val flow1 = MutableStateFlow(false)
             val flow2 = MutableStateFlow(false)
-            val values by collectValues(or(flow1, flow2))
+            val values by collectValues(anyOf(flow1, flow2))
 
             assertThat(values).containsExactly(false)
             flow1.value = true
diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml
index c43e394..da12dd7 100644
--- a/packages/SystemUI/res-keyguard/values/styles.xml
+++ b/packages/SystemUI/res-keyguard/values/styles.xml
@@ -36,7 +36,6 @@
     </style>
     <style name="Keyguard.Bouncer.SecondaryMessage" parent="Theme.SystemUI">
         <item name="android:textSize">14sp</item>
-        <item name="android:lineHeight">20dp</item>
         <item name="android:maxLines">@integer/bouncer_secondary_message_lines</item>
         <item name="android:lines">@integer/bouncer_secondary_message_lines</item>
         <item name="android:textAlignment">center</item>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a1daebd..5857692 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -285,6 +285,9 @@
     the amount by the view is positioned above the screen before the animation starts. -->
     <dimen name="heads_up_appear_y_above_screen">32dp</dimen>
 
+    <!-- padding between the old and new heads up notifications for the hun cycling animation -->
+    <dimen name="heads_up_cycling_padding">8dp</dimen>
+
     <!-- padding between the heads up and the statusbar -->
     <dimen name="heads_up_status_bar_padding">8dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 45bcd82..b5ec5b2 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1158,6 +1158,8 @@
     <string name="button_to_configure_widgets_text">Customize widgets</string>
     <!-- Description for the App icon of disabled widget. [CHAR LIMIT=NONE] -->
     <string name="icon_description_for_disabled_widget">App icon for disabled widget</string>
+    <!-- Description for the App icon of a package that is currently being installed. [CHAR LIMIT=NONE] -->
+    <string name="icon_description_for_pending_widget">App icon for a widget being installed</string>
     <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] -->
     <string name="edit_widget">Edit widget</string>
     <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
@@ -1634,9 +1636,15 @@
     <string name="volume_panel_collapsed_sliders">Volume sliders collapsed</string>
 
     <!-- Hint for accessibility. A stream name is a parameter. For example: double tap to mute media [CHAR_LIMIT=NONE] -->
-    <string name="volume_panel_hint_mute">mute %s</string>
+    <string name="volume_panel_hint_mute">Mute %s</string>
     <!-- Hint for accessibility. A stream name is a parameter. For example: double tap to unmute media [CHAR_LIMIT=NONE] -->
-    <string name="volume_panel_hint_unmute">unmute %s</string>
+    <string name="volume_panel_hint_unmute">Unmute %s</string>
+
+    <!-- Hint for accessibility. This is announced when the stream is muted [CHAR_LIMIT=NONE] -->
+    <string name="volume_panel_hint_muted">muted</string>
+
+    <!-- Hint for accessibility. This is announced when ring mode is set to Vibrate. [CHAR_LIMIT=NONE] -->
+    <string name="volume_panel_hint_vibrate">vibrate</string>
 
     <!-- Title with application label for media output settings when there is media playing. [CHAR LIMIT=20] -->
     <string name="media_output_label_title">Playing <xliff:g id="label" example="Music Player">%s</xliff:g> on</string>
diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
index 422f02f..8979ef1 100644
--- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
@@ -16,7 +16,6 @@
 package com.android.systemui.biometrics
 
 import android.Manifest
-import android.annotation.IntDef
 import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
 import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
 import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX
@@ -39,14 +38,9 @@
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityManager
 import com.android.internal.widget.LockPatternUtils
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
+import com.android.systemui.biometrics.shared.model.PromptKind
 
 object Utils {
-    const val CREDENTIAL_PIN = 1
-    const val CREDENTIAL_PATTERN = 2
-    const val CREDENTIAL_PASSWORD = 3
-
     /** Base set of layout flags for fingerprint overlay widgets. */
     const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS =
         (WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
@@ -91,17 +85,16 @@
         (promptInfo.authenticators and Authenticators.BIOMETRIC_WEAK) != 0
 
     @JvmStatic
-    @CredentialType
-    fun getCredentialType(utils: LockPatternUtils, userId: Int): Int =
+    fun getCredentialType(utils: LockPatternUtils, userId: Int): PromptKind =
         when (utils.getKeyguardStoredPasswordQuality(userId)) {
-            PASSWORD_QUALITY_SOMETHING -> CREDENTIAL_PATTERN
+            PASSWORD_QUALITY_SOMETHING -> PromptKind.Pattern
             PASSWORD_QUALITY_NUMERIC,
-            PASSWORD_QUALITY_NUMERIC_COMPLEX -> CREDENTIAL_PIN
+            PASSWORD_QUALITY_NUMERIC_COMPLEX -> PromptKind.Pin
             PASSWORD_QUALITY_ALPHABETIC,
             PASSWORD_QUALITY_ALPHANUMERIC,
             PASSWORD_QUALITY_COMPLEX,
-            PASSWORD_QUALITY_MANAGED -> CREDENTIAL_PASSWORD
-            else -> CREDENTIAL_PASSWORD
+            PASSWORD_QUALITY_MANAGED -> PromptKind.Password
+            else -> PromptKind.Password
         }
 
     @JvmStatic
@@ -129,8 +122,4 @@
         return windowMetrics?.windowInsets?.getInsets(WindowInsets.Type.navigationBars())
             ?: Insets.NONE
     }
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef(CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD)
-    annotation class CredentialType
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
similarity index 69%
rename from packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
rename to packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
index a97e2dc..8782962 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/PromptKind.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/PromptKind.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.
@@ -16,16 +16,20 @@
 
 package com.android.systemui.biometrics.shared.model
 
-import com.android.systemui.biometrics.Utils
-
-// TODO(b/251476085): this should eventually replace Utils.CredentialType
-/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
 sealed interface PromptKind {
+    object None : PromptKind
+
     data class Biometric(
         val activeModalities: BiometricModalities = BiometricModalities(),
+        // TODO(b/330908557): Use this value to decide whether to show two pane layout, instead of
+        // simply depending on rotations.
+        val showTwoPane: Boolean = false
     ) : PromptKind
 
     object Pin : PromptKind
     object Pattern : PromptKind
     object Password : PromptKind
+
+    fun isBiometric() = this is Biometric
+    fun isCredential() = (this is Pin) or (this is Pattern) or (this is Password)
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 460779c..f33acf2 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -432,6 +433,7 @@
                         listenForDozeAmountTransition(this)
                         listenForAnyStateToAodTransition(this)
                         listenForAnyStateToLockscreenTransition(this)
+                        listenForAnyStateToDozingTransition(this)
                     } else {
                         listenForDozeAmount(this)
                     }
@@ -578,6 +580,21 @@
         }
     }
 
+    /**
+     * When keyguard is displayed due to pulsing notifications when AOD is off,
+     * we should make sure clock is in dozing state instead of LS state
+     */
+    @VisibleForTesting
+    internal fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardTransitionInteractor
+                    .transitionStepsToState(DOZING)
+                    .filter { it.transitionState == TransitionState.FINISHED }
+                    .collect { handleDoze(1f) }
+        }
+    }
+
+
     @VisibleForTesting
     internal fun listenForDozing(scope: CoroutineScope): Job {
         return scope.launch {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 91fb688..905a98c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -35,6 +35,7 @@
 
 import android.app.ActivityManager;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
 import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
@@ -134,6 +135,7 @@
     private final BouncerMessageInteractor mBouncerMessageInteractor;
     private int mTranslationY;
     private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private final DevicePolicyManager mDevicePolicyManager;
     // Whether the volume keys should be handled by keyguard. If true, then
     // they will be handled here for specific media types such as music, otherwise
     // the audio service will bring up the volume dialog.
@@ -460,6 +462,7 @@
             SelectedUserInteractor selectedUserInteractor,
             DeviceProvisionedController deviceProvisionedController,
             FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
+            DevicePolicyManager devicePolicyManager,
             KeyguardTransitionInteractor keyguardTransitionInteractor,
             Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
             Provider<DeviceEntryInteractor> deviceEntryInteractor
@@ -495,6 +498,7 @@
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mDeviceProvisionedController = deviceProvisionedController;
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
+        mDevicePolicyManager = devicePolicyManager;
     }
 
     @Override
@@ -1105,35 +1109,23 @@
 
         if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts);
 
-        final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager();
         final int failedAttemptsBeforeWipe =
-                dpm.getMaximumFailedPasswordsForWipe(null, userId);
+                mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId);
 
         final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0
                 ? (failedAttemptsBeforeWipe - failedAttempts)
                 : Integer.MAX_VALUE; // because DPM returns 0 if no restriction
         if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) {
-            // The user has installed a DevicePolicyManager that requests a user/profile to be wiped
-            // N attempts. Once we get below the grace period, we post this dialog every time as a
-            // clear warning until the deletion fires.
-            // Check which profile has the strictest policy for failed password attempts
-            final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId);
-            int userType = USER_TYPE_PRIMARY;
-            if (expiringUser == userId) {
-                // TODO: http://b/23522538
-                if (expiringUser != UserHandle.USER_SYSTEM) {
-                    userType = USER_TYPE_SECONDARY_USER;
-                }
-            } else if (expiringUser != UserHandle.USER_NULL) {
-                userType = USER_TYPE_WORK_PROFILE;
-            } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
-            if (remainingBeforeWipe > 0) {
-                mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType);
-            } else {
-                // Too many attempts. The device will be wiped shortly.
-                Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!");
-                mView.showWipeDialog(failedAttempts, userType);
-            }
+            // The user has installed a DevicePolicyManager that requests a
+            // user/profile to be wiped N attempts. Once we get below the grace period,
+            // we post this dialog every time as a clear warning until the deletion
+            // fires. Check which profile has the strictest policy for failed password
+            // attempts.
+            final int expiringUser =
+                    mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId);
+            Integer mainUser = mSelectedUserInteractor.getMainUserId();
+            showMessageForFailedUnlockAttempt(
+                    userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts);
         }
         mLockPatternUtils.reportFailedPasswordAttempt(userId);
         if (timeoutMs > 0) {
@@ -1145,6 +1137,35 @@
         }
     }
 
+    @VisibleForTesting
+    void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId,
+            int remainingBeforeWipe, int failedAttempts) {
+        int userType = USER_TYPE_PRIMARY;
+        if (expiringUserId == userId) {
+            int primaryUser = UserHandle.USER_SYSTEM;
+            if (Flags.headlessSingleUserFixes()) {
+                if (mainUserId != null) {
+                    primaryUser = mainUserId;
+                }
+            }
+            // TODO: http://b/23522538
+            if (expiringUserId != primaryUser) {
+                userType = USER_TYPE_SECONDARY_USER;
+            }
+        } else if (expiringUserId != UserHandle.USER_NULL) {
+            userType = USER_TYPE_WORK_PROFILE;
+        } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
+        if (remainingBeforeWipe > 0) {
+            mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe,
+                    userType);
+        } else {
+            // Too many attempts. The device will be wiped shortly.
+            Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId
+                    + " will be wiped!");
+            mView.showWipeDialog(failedAttempts, userType);
+        }
+    }
+
     private void getCurrentSecurityController(
             KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) {
         mSecurityViewFlipperController
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 177d933..35c2024 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -30,6 +30,8 @@
 import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.SurfaceControl;
@@ -41,6 +43,8 @@
 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
 import android.window.InputTransferToken;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.CoreStartable;
@@ -69,6 +73,9 @@
 public class Magnification implements CoreStartable, CommandQueue.Callbacks {
     private static final String TAG = "Magnification";
 
+    @VisibleForTesting static final int DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS = 300;
+    private static final int MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL = 1;
+
     private final ModeSwitchesController mModeSwitchesController;
     private final Context mContext;
     private final Handler mHandler;
@@ -209,8 +216,26 @@
             SysUiState sysUiState, OverviewProxyService overviewProxyService,
             SecureSettings secureSettings, DisplayTracker displayTracker,
             DisplayManager displayManager, AccessibilityLogger a11yLogger) {
+        this(context, mainHandler.getLooper(), executor, commandQueue,
+                modeSwitchesController, sysUiState, overviewProxyService, secureSettings,
+                displayTracker, displayManager, a11yLogger);
+    }
+
+    @VisibleForTesting
+    public Magnification(Context context, Looper looper, @Main Executor executor,
+            CommandQueue commandQueue, ModeSwitchesController modeSwitchesController,
+            SysUiState sysUiState, OverviewProxyService overviewProxyService,
+            SecureSettings secureSettings, DisplayTracker displayTracker,
+            DisplayManager displayManager, AccessibilityLogger a11yLogger) {
         mContext = context;
-        mHandler = mainHandler;
+        mHandler = new Handler(looper) {
+            @Override
+            public void handleMessage(@NonNull Message msg) {
+                if (msg.what == MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL) {
+                    showMagnificationButtonInternal(msg.arg1, msg.arg2);
+                }
+            }
+        };
         mExecutor = executor;
         mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
         mCommandQueue = commandQueue;
@@ -350,6 +375,21 @@
 
     @MainThread
     void showMagnificationButton(int displayId, int magnificationMode) {
+        if (Flags.delayShowMagnificationButton()) {
+            if (mHandler.hasMessages(MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL)) {
+                return;
+            }
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(
+                            MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL, displayId, magnificationMode),
+                    DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS);
+        } else {
+            showMagnificationButtonInternal(displayId, magnificationMode);
+        }
+    }
+
+    @MainThread
+    private void showMagnificationButtonInternal(int displayId, int magnificationMode) {
         // not to show mode switch button if settings panel is already showing to
         // prevent settings panel be covered by the button.
         if (isMagnificationSettingsPanelShowing(displayId)) {
@@ -360,6 +400,9 @@
 
     @MainThread
     void removeMagnificationButton(int displayId) {
+        if (Flags.delayShowMagnificationButton()) {
+            mHandler.removeMessages(MSG_SHOW_MAGNIFICATION_BUTTON_INTERNAL);
+        }
         mModeSwitchesController.removeButton(displayId);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
new file mode 100644
index 0000000..8f071e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.model
+
+import java.time.LocalTime
+
+sealed interface NightDisplayChangeEvent {
+    data class OnAutoModeChanged(val autoMode: Int) : NightDisplayChangeEvent
+    data class OnActivatedChanged(val isActivated: Boolean) : NightDisplayChangeEvent
+    data class OnCustomStartTimeChanged(val startTime: LocalTime?) : NightDisplayChangeEvent
+    data class OnCustomEndTimeChanged(val endTime: LocalTime?) : NightDisplayChangeEvent
+    data class OnForceAutoModeChanged(val shouldForceAutoMode: Boolean) : NightDisplayChangeEvent
+    data class OnLocationEnabledChanged(val locationEnabled: Boolean) : NightDisplayChangeEvent
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
new file mode 100644
index 0000000..196876e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.model
+
+import java.time.LocalTime
+
+/** models the state of NightDisplayRepository */
+data class NightDisplayState(
+    val autoMode: Int = 0,
+    val isActivated: Boolean = true,
+    val startTime: LocalTime? = null,
+    val endTime: LocalTime? = null,
+    val shouldForceAutoMode: Boolean = false,
+    val locationEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt
index ae9f57f..6032f0b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt
@@ -18,7 +18,7 @@
 
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import dagger.Module
 import dagger.Provides
 import kotlinx.coroutines.channels.awaitClose
@@ -29,6 +29,8 @@
 interface AccessibilityRepository {
     /** @see [AccessibilityManager.isTouchExplorationEnabled] */
     val isTouchExplorationEnabled: Flow<Boolean>
+    /** @see [AccessibilityManager.isEnabled] */
+    val isEnabled: Flow<Boolean>
 
     companion object {
         operator fun invoke(a11yManager: AccessibilityManager): AccessibilityRepository =
@@ -47,6 +49,15 @@
                 awaitClose { manager.removeTouchExplorationStateChangeListener(listener) }
             }
             .distinctUntilChanged()
+
+    override val isEnabled: Flow<Boolean> =
+        conflatedCallbackFlow {
+                val listener = AccessibilityManager.AccessibilityStateChangeListener(::trySend)
+                manager.addAccessibilityStateChangeListener(listener)
+                trySend(manager.isEnabled)
+                awaitClose { manager.removeAccessibilityStateChangeListener(listener) }
+            }
+            .distinctUntilChanged()
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
new file mode 100644
index 0000000..bf44fab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.accessibility.data.model.NightDisplayChangeEvent
+import com.android.systemui.accessibility.data.model.NightDisplayState
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+class NightDisplayRepository
+@Inject
+constructor(
+    @Background private val bgCoroutineContext: CoroutineContext,
+    @Application private val scope: CoroutineScope,
+    private val globalSettings: GlobalSettings,
+    private val secureSettings: SecureSettings,
+    private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
+    private val colorDisplayManagerUserScopedService: UserScopedService<ColorDisplayManager>,
+    private val locationController: LocationController,
+) {
+    private val stateFlowUserMap = mutableMapOf<Int, Flow<NightDisplayState>>()
+
+    fun nightDisplayState(user: UserHandle): Flow<NightDisplayState> =
+        stateFlowUserMap.getOrPut(user.identifier) {
+            return merge(
+                    colorDisplayManagerChangeEventFlow(user),
+                    shouldForceAutoMode(user).map {
+                        NightDisplayChangeEvent.OnForceAutoModeChanged(it)
+                    },
+                    locationController.isLocationEnabledFlow().map {
+                        NightDisplayChangeEvent.OnLocationEnabledChanged(it)
+                    }
+                )
+                .scan(initialState(user)) { state, event ->
+                    when (event) {
+                        is NightDisplayChangeEvent.OnActivatedChanged ->
+                            state.copy(isActivated = event.isActivated)
+                        is NightDisplayChangeEvent.OnAutoModeChanged ->
+                            state.copy(autoMode = event.autoMode)
+                        is NightDisplayChangeEvent.OnCustomStartTimeChanged ->
+                            state.copy(startTime = event.startTime)
+                        is NightDisplayChangeEvent.OnCustomEndTimeChanged ->
+                            state.copy(endTime = event.endTime)
+                        is NightDisplayChangeEvent.OnForceAutoModeChanged ->
+                            state.copy(shouldForceAutoMode = event.shouldForceAutoMode)
+                        is NightDisplayChangeEvent.OnLocationEnabledChanged ->
+                            state.copy(locationEnabled = event.locationEnabled)
+                    }
+                }
+                .conflate()
+                .onStart { emit(initialState(user)) }
+                .flowOn(bgCoroutineContext)
+                .stateIn(scope, SharingStarted.WhileSubscribed(), NightDisplayState())
+        }
+
+    /** Track changes in night display enabled state and its auto mode */
+    private fun colorDisplayManagerChangeEventFlow(user: UserHandle) = callbackFlow {
+        val nightDisplayListener = nightDisplayListenerBuilder.setUser(user.identifier).build()
+        val nightDisplayCallback =
+            object : NightDisplayListener.Callback {
+                override fun onActivated(activated: Boolean) {
+                    trySend(NightDisplayChangeEvent.OnActivatedChanged(activated))
+                }
+
+                override fun onAutoModeChanged(autoMode: Int) {
+                    trySend(NightDisplayChangeEvent.OnAutoModeChanged(autoMode))
+                }
+
+                override fun onCustomStartTimeChanged(startTime: LocalTime?) {
+                    trySend(NightDisplayChangeEvent.OnCustomStartTimeChanged(startTime))
+                }
+
+                override fun onCustomEndTimeChanged(endTime: LocalTime?) {
+                    trySend(NightDisplayChangeEvent.OnCustomEndTimeChanged(endTime))
+                }
+            }
+        nightDisplayListener.setCallback(nightDisplayCallback)
+        awaitClose { nightDisplayListener.setCallback(null) }
+    }
+
+    /** @return true when the option to force auto mode is available and a value has not been set */
+    private fun shouldForceAutoMode(userHandle: UserHandle): Flow<Boolean> =
+        combine(isForceAutoModeAvailable, isDisplayAutoModeRawNotSet(userHandle)) {
+            isForceAutoModeAvailable,
+            isDisplayAutoModeRawNotSet,
+            ->
+            isForceAutoModeAvailable && isDisplayAutoModeRawNotSet
+        }
+
+    private val isForceAutoModeAvailable: Flow<Boolean> =
+        globalSettings
+            .observerFlow(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME)
+            .onStart { emit(Unit) }
+            .map {
+                globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+                    NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+            }
+            .distinctUntilChanged()
+
+    /** Inspired by [ColorDisplayService.getNightDisplayAutoModeRawInternal] */
+    private fun isDisplayAutoModeRawNotSet(userHandle: UserHandle): Flow<Boolean> =
+        if (userHandle.identifier == UserHandle.USER_NULL) {
+                flowOf(IS_AUTO_MODE_RAW_NOT_SET_DEFAULT)
+            } else {
+                secureSettings
+                    .observerFlow(userHandle.identifier, DISPLAY_AUTO_MODE_RAW_SETTING_NAME)
+                    .onStart { emit(Unit) }
+                    .map {
+                        secureSettings.getIntForUser(
+                            DISPLAY_AUTO_MODE_RAW_SETTING_NAME,
+                            userHandle.identifier
+                        ) == NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET
+                    }
+            }
+            .distinctUntilChanged()
+
+    suspend fun setNightDisplayAutoMode(autoMode: Int, user: UserHandle) {
+        withContext(bgCoroutineContext) {
+            colorDisplayManagerUserScopedService.forUser(user).nightDisplayAutoMode = autoMode
+        }
+    }
+
+    suspend fun setNightDisplayActivated(activated: Boolean, user: UserHandle) {
+        withContext(bgCoroutineContext) {
+            colorDisplayManagerUserScopedService.forUser(user).isNightDisplayActivated = activated
+        }
+    }
+
+    private fun initialState(user: UserHandle): NightDisplayState {
+        val colorDisplayManager = colorDisplayManagerUserScopedService.forUser(user)
+        return NightDisplayState(
+            colorDisplayManager.nightDisplayAutoMode,
+            colorDisplayManager.isNightDisplayActivated,
+            colorDisplayManager.nightDisplayCustomStartTime,
+            colorDisplayManager.nightDisplayCustomEndTime,
+            globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+                NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE &&
+                secureSettings.getIntForUser(DISPLAY_AUTO_MODE_RAW_SETTING_NAME, user.identifier) ==
+                    NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET,
+            locationController.isLocationEnabled,
+        )
+    }
+
+    private companion object {
+        const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+        const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+        const val IS_AUTO_MODE_RAW_NOT_SET_DEFAULT = true
+        const val IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME =
+            Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+        const val DISPLAY_AUTO_MODE_RAW_SETTING_NAME = Settings.Secure.NIGHT_DISPLAY_AUTO_MODE
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt
index 968ce0d..93b624a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt
@@ -28,6 +28,8 @@
     private val a11yRepo: AccessibilityRepository,
 ) {
     /** @see [android.view.accessibility.AccessibilityManager.isTouchExplorationEnabled] */
-    val isTouchExplorationEnabled: Flow<Boolean>
-        get() = a11yRepo.isTouchExplorationEnabled
+    val isTouchExplorationEnabled: Flow<Boolean> = a11yRepo.isTouchExplorationEnabled
+
+    /** @see [android.view.accessibility.AccessibilityManager.isEnabled] */
+    val isEnabled: Flow<Boolean> = a11yRepo.isEnabled
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 54dd6d0..ed9597d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -41,6 +41,10 @@
 import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
 import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
 import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.ui.NightDisplayTileMapper
 import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileDataInteractor
 import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileUserActionInteractor
 import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
@@ -117,6 +121,7 @@
         const val FONT_SCALING_TILE_SPEC = "font_scaling"
         const val REDUCE_BRIGHTNESS_TILE_SPEC = "reduce_brightness"
         const val ONE_HANDED_TILE_SPEC = "onehanded"
+        const val NIGHT_DISPLAY_TILE_SPEC = "night"
 
         @Provides
         @IntoMap
@@ -279,5 +284,41 @@
                     mapper,
                 )
             else StubQSTileViewModel
+
+        @Provides
+        @IntoMap
+        @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+        fun provideNightDisplayTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+            QSTileConfig(
+                tileSpec = TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = R.drawable.qs_nightlight_icon_off,
+                        labelRes = R.string.quick_settings_night_display_label,
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+            )
+
+        /**
+         * Inject NightDisplay Tile into tileViewModelMap in QSModule. The tile is hidden behind a
+         * flag.
+         */
+        @Provides
+        @IntoMap
+        @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+        fun provideNightDisplayTileViewModel(
+            factory: QSTileViewModelFactory.Static<NightDisplayTileModel>,
+            mapper: NightDisplayTileMapper,
+            stateInteractor: NightDisplayTileDataInteractor,
+            userActionInteractor: NightDisplayTileUserActionInteractor
+        ): QSTileViewModel =
+            if (Flags.qsNewTilesFuture())
+                factory.create(
+                    TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+                    userActionInteractor,
+                    stateInteractor,
+                    mapper,
+                )
+            else StubQSTileViewModel
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5df7fc9..fcba425 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.authentication.domain.interactor
 
+import android.app.admin.flags.Flags
 import android.os.UserHandle
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockPatternView
@@ -288,9 +289,15 @@
     private suspend fun getWipeTarget(): WipeTarget {
         // Check which profile has the strictest policy for failed authentication attempts.
         val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe()
+        val primaryUser =
+            if (Flags.headlessSingleUserFixes()) {
+                selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM
+            } else {
+                UserHandle.USER_SYSTEM
+            }
         return when (userToBeWiped) {
             selectedUserInteractor.getSelectedUserId() ->
-                if (userToBeWiped == UserHandle.USER_SYSTEM) {
+                if (userToBeWiped == primaryUser) {
                     WipeTarget.WholeDevice
                 } else {
                     WipeTarget.User
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 5ba0b2d..9ba41ef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -76,6 +76,7 @@
 import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
 import com.android.systemui.biometrics.shared.model.BiometricModalities;
+import com.android.systemui.biometrics.shared.model.PromptKind;
 import com.android.systemui.biometrics.ui.BiometricPromptLayout;
 import com.android.systemui.biometrics.ui.CredentialView;
 import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
@@ -500,24 +501,18 @@
     private void addCredentialView(boolean animatePanel, boolean animateContents) {
         final LayoutInflater factory = LayoutInflater.from(mContext);
 
-        @Utils.CredentialType final int credentialType = Utils.getCredentialType(
-                mLockPatternUtils, mEffectiveUserId);
-
-        switch (credentialType) {
-            case Utils.CREDENTIAL_PATTERN:
-                mCredentialView = factory.inflate(
-                        R.layout.auth_credential_pattern_view, null, false);
-                break;
-            case Utils.CREDENTIAL_PIN:
-                mCredentialView = factory.inflate(R.layout.auth_credential_pin_view, null, false);
-                break;
-            case Utils.CREDENTIAL_PASSWORD:
-                mCredentialView = factory.inflate(
-                        R.layout.auth_credential_password_view, null, false);
-                break;
-            default:
-                throw new IllegalStateException("Unknown credential type: " + credentialType);
+        PromptKind credentialType = Utils.getCredentialType(mLockPatternUtils, mEffectiveUserId);
+        final int layoutResourceId;
+        if (credentialType instanceof PromptKind.Pattern) {
+            layoutResourceId = R.layout.auth_credential_pattern_view;
+        } else if (credentialType instanceof PromptKind.Pin) {
+            layoutResourceId = R.layout.auth_credential_pin_view;
+        } else if (credentialType instanceof PromptKind.Password) {
+            layoutResourceId = R.layout.auth_credential_password_view;
+        } else {
+            throw new IllegalStateException("Unknown credential type: " + credentialType);
         }
+        mCredentialView = factory.inflate(layoutResourceId, null, false);
 
         // The background is used for detecting taps / cancelling authentication. Since the
         // credential view is full-screen and should not be canceled from background taps,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
index 9ad3f43..f659ff0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -125,7 +125,7 @@
     private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
     override val userId = _userId.asStateFlow()
 
-    private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())
+    private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.None)
     override val kind = _kind.asStateFlow()
 
     private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null)
@@ -149,7 +149,7 @@
     override val showBpWithoutIconForCredential = _showBpWithoutIconForCredential.asStateFlow()
 
     override fun setShouldShowBpWithoutIconForCredential(promptInfo: PromptInfo) {
-        val hasCredentialViewShown = kind.value !is PromptKind.Biometric
+        val hasCredentialViewShown = kind.value.isCredential()
         val showBpForCredential =
             Flags.customBiometricPrompt() &&
                 constraintBp() &&
@@ -178,7 +178,7 @@
         _promptInfo.value = null
         _userId.value = null
         _challenge.value = null
-        _kind.value = PromptKind.Biometric()
+        _kind.value = PromptKind.None
         _opPackageName.value = null
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
index b7c0fa8..f8fb7bb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt
@@ -16,10 +16,8 @@
 
 package com.android.systemui.biometrics.domain.interactor
 
-import android.hardware.biometrics.PromptInfo
 import com.android.internal.widget.LockPatternView
 import com.android.internal.widget.LockscreenCredential
-import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.data.repository.PromptRepository
 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
@@ -42,12 +40,6 @@
  * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users
  * PIN, pattern, or password credential instead of a biometric.
  *
- * This is used to cache the calling app's options that were given to the underlying authenticate
- * APIs and should be set before any UI is shown to the user.
- *
- * There can be at most one request active at a given time. Use [resetPrompt] when no request is
- * active to clear the cache.
- *
  * Views that use any biometric should use [PromptSelectorInteractor] instead.
  */
 class PromptCredentialInteractor
@@ -137,28 +129,6 @@
     private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null)
     val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow()
 
-    /** Update the current request to use credential-based authentication instead of biometrics. */
-    fun useCredentialsForAuthentication(
-        promptInfo: PromptInfo,
-        @Utils.CredentialType kind: Int,
-        userId: Int,
-        challenge: Long,
-        opPackageName: String,
-    ) {
-        biometricPromptRepository.setPrompt(
-            promptInfo,
-            userId,
-            challenge,
-            kind.asBiometricPromptCredential(),
-            opPackageName,
-        )
-    }
-
-    /** Unset the current authentication request. */
-    fun resetPrompt() {
-        biometricPromptRepository.unsetPrompt()
-    }
-
     /**
      * Check a credential and return the attestation token (HAT) if successful.
      *
@@ -231,13 +201,3 @@
         _verificationError.value = null
     }
 }
-
-// TODO(b/251476085): remove along with Utils.CredentialType
-/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
-private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
-    when (this) {
-        Utils.CREDENTIAL_PIN -> PromptKind.Pin
-        Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
-        Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
-        else -> PromptKind.Biometric()
-    }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index 45816c1..deb47d3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -18,7 +18,6 @@
 
 import android.hardware.biometrics.PromptInfo
 import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.Utils.getCredentialType
 import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -95,7 +94,7 @@
     /** Use credential-based authentication instead of biometrics. */
     fun useCredentialsForAuthentication(
         promptInfo: PromptInfo,
-        @Utils.CredentialType kind: Int,
+        kind: PromptKind,
         userId: Int,
         challenge: Long,
         opPackageName: String,
@@ -152,14 +151,7 @@
     override val credentialKind: Flow<PromptKind> =
         combine(prompt, isCredentialAllowed) { prompt, isAllowed ->
             if (prompt != null && isAllowed) {
-                when (
-                    getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
-                ) {
-                    Utils.CREDENTIAL_PIN -> PromptKind.Pin
-                    Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
-                    Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
-                    else -> PromptKind.Biometric()
-                }
+                getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
             } else {
                 PromptKind.Biometric()
             }
@@ -191,7 +183,7 @@
 
     override fun useCredentialsForAuthentication(
         promptInfo: PromptInfo,
-        @Utils.CredentialType kind: Int,
+        kind: PromptKind,
         userId: Int,
         challenge: Long,
         opPackageName: String,
@@ -200,7 +192,7 @@
             promptInfo = promptInfo,
             userId = userId,
             gatekeeperChallenge = challenge,
-            kind = kind.asBiometricPromptCredential(),
+            kind = kind,
             opPackageName = opPackageName,
         )
     }
@@ -209,13 +201,3 @@
         promptRepository.unsetPrompt()
     }
 }
-
-// TODO(b/251476085): remove along with Utils.CredentialType
-/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */
-private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind =
-    when (this) {
-        Utils.CREDENTIAL_PIN -> PromptKind.Pin
-        Utils.CREDENTIAL_PASSWORD -> PromptKind.Password
-        Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern
-        else -> PromptKind.Biometric()
-    }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt
new file mode 100644
index 0000000..2e9169e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface BluetoothTileDialogModule {
+    @Binds
+    @SysUISingleton
+    fun bindDeviceItemActionInteractor(
+        impl: DeviceItemActionInteractorImpl
+    ): DeviceItemActionInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index 4369f3f..94f465d 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -62,6 +62,7 @@
 @Inject
 constructor(
     private val deviceItemInteractor: DeviceItemInteractor,
+    private val deviceItemActionInteractor: DeviceItemActionInteractor,
     private val bluetoothStateInteractor: BluetoothStateInteractor,
     private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
     private val audioSharingInteractor: AudioSharingInteractor,
@@ -192,7 +193,7 @@
 
                 // deviceItemClick is emitted when user clicked on a device item.
                 dialogDelegate.deviceItemClick
-                    .onEach { deviceItemInteractor.updateDeviceItemOnClick(it) }
+                    .onEach { deviceItemActionInteractor.onClick(it, dialog) }
                     .launchIn(this)
 
                 // contentHeight is emitted when the dialog is dismissed.
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
new file mode 100644
index 0000000..9311760
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Defines interface for click handling of a DeviceItem. */
+interface DeviceItemActionInteractor {
+    suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+}
+
+@SysUISingleton
+open class DeviceItemActionInteractorImpl
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val logger: BluetoothTileDialogLogger,
+    private val uiEventLogger: UiEventLogger,
+) : DeviceItemActionInteractor {
+
+    override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+        withContext(backgroundDispatcher) {
+            logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
+
+            deviceItem.cachedBluetoothDevice.apply {
+                when (deviceItem.type) {
+                    DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
+                        disconnect()
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT)
+                    }
+                    DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
+                    }
+                    DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
+                        setActive()
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
+                    }
+                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {
+                        disconnect()
+                        uiEventLogger.log(
+                            BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT
+                        )
+                    }
+                    DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+                        connect()
+                        uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 66e593b..1526cd9 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -20,7 +20,6 @@
 import android.bluetooth.BluetoothDevice
 import android.content.Context
 import android.media.AudioManager
-import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.BluetoothCallback
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -52,7 +51,6 @@
     private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
     private val localBluetoothManager: LocalBluetoothManager?,
     private val systemClock: SystemClock,
-    private val uiEventLogger: UiEventLogger,
     private val logger: BluetoothTileDialogLogger,
     @Application private val coroutineScope: CoroutineScope,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
@@ -169,38 +167,6 @@
         )
     }
 
-    internal suspend fun updateDeviceItemOnClick(deviceItem: DeviceItem) {
-        withContext(backgroundDispatcher) {
-            logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
-
-            deviceItem.cachedBluetoothDevice.apply {
-                when (deviceItem.type) {
-                    DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
-                        disconnect()
-                        uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT)
-                    }
-                    DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
-                        uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
-                    }
-                    DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
-                        setActive()
-                        uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
-                    }
-                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {
-                        disconnect()
-                        uiEventLogger.log(
-                            BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT
-                        )
-                    }
-                    DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
-                        connect()
-                        uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
-                    }
-                }
-            }
-        }
-    }
-
     internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
         deviceItemFactoryList = list
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index fa19bf4..e0334a0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -29,7 +29,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.time.SystemClock
 import dagger.Lazy
 import javax.inject.Inject
@@ -78,7 +78,7 @@
             bouncerRepository.alternateBouncerUIAvailable
         }
     private val isDozingOrAod: Flow<Boolean> =
-        or(
+        anyOf(
                 keyguardTransitionInteractor.get().transitionValue(KeyguardState.DOZING).map {
                     it > 0f
                 },
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index beaa170..b42a903 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -206,7 +206,7 @@
         );
         final CommunalInteractor communalInteractor = mCommunalInteractorLazy.get();
         mJavaAdapter.alwaysCollectFlow(
-                BooleanFlowOperators.INSTANCE.and(
+                BooleanFlowOperators.INSTANCE.allOf(
                         communalInteractor.isCommunalEnabled(),
                         communalInteractor.isCommunalShowing()),
                 this::onShowingCommunalHubChanged
@@ -292,6 +292,7 @@
 
     @Override
     public void onKeyEvent(KeyEvent ev) {
+        logDebug("REAL: onKeyEvent(" + KeyEvent.actionToString(ev.getAction()) + ")");
         // Only collect if it is an ACTION_UP action and is allow-listed
         if (ev.getAction() == KeyEvent.ACTION_UP && mAcceptedKeycodes.contains(ev.getKeyCode())) {
             mFalsingDataProvider.onKeyEvent(ev);
@@ -300,7 +301,7 @@
 
     @Override
     public void onTouchEvent(MotionEvent ev) {
-        logDebug("REAL: onTouchEvent(" + ev.getActionMasked() + ")");
+        logDebug("REAL: onTouchEvent(" + MotionEvent.actionToString(ev.getActionMasked()) + ")");
         if (!mKeyguardStateController.isShowing()) {
             avoidGesture();
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt
index b289fa4..6b22137 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt
@@ -61,11 +61,11 @@
     }
 
     override fun onKeyEvent(ev: KeyEvent) {
-        logDebug("NOOP: onKeyEvent(${ev.action}")
+        logDebug("NOOP: onKeyEvent(${KeyEvent.actionToString(ev.action)}")
     }
 
     override fun onTouchEvent(ev: MotionEvent) {
-        logDebug("NOOP: onTouchEvent(${ev.actionMasked})")
+        logDebug("NOOP: onTouchEvent(${MotionEvent.actionToString(ev.actionMasked)})")
     }
 
     override fun onMotionEventComplete() {
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
index 5c64dc6..1c16429 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
@@ -18,6 +18,7 @@
 
 import android.os.UserHandle
 import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
 import kotlinx.coroutines.flow.Flow
 
 interface PackageChangeRepository {
@@ -28,4 +29,7 @@
      * [UserHandle.USER_ALL] may be used to listen to all users.
      */
     fun packageChanged(user: UserHandle): Flow<PackageChangeModel>
+
+    /** Emits a list of all known install sessions associated with the primary user. */
+    val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
index 712a352..41b03f1 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
@@ -18,6 +18,7 @@
 
 import android.os.UserHandle
 import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -27,6 +28,7 @@
 class PackageChangeRepositoryImpl
 @Inject
 constructor(
+    packageInstallerMonitor: PackageInstallerMonitor,
     private val monitorFactory: PackageUpdateMonitor.Factory,
 ) : PackageChangeRepository {
     /**
@@ -37,4 +39,7 @@
 
     override fun packageChanged(user: UserHandle): Flow<PackageChangeModel> =
         monitor.packageChanged.filter { user == UserHandle.ALL || user == it.user }
+
+    override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+        packageInstallerMonitor.installSessionsForPrimaryUser
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
new file mode 100644
index 0000000..46db346
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.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.common.data.repository
+
+import android.content.pm.PackageInstaller
+import android.os.Handler
+import com.android.internal.annotations.GuardedBy
+import com.android.systemui.common.shared.model.PackageInstallSession
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.PackageChangeRepoLog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/** Monitors package install sessions for all users. */
+@SysUISingleton
+class PackageInstallerMonitor
+@Inject
+constructor(
+    @Background private val bgHandler: Handler,
+    @Background private val bgScope: CoroutineScope,
+    @PackageChangeRepoLog logBuffer: LogBuffer,
+    private val packageInstaller: PackageInstaller,
+) : PackageInstaller.SessionCallback() {
+
+    private val logger = Logger(logBuffer, TAG)
+
+    @GuardedBy("sessions") private val sessions = mutableMapOf<Int, PackageInstallSession>()
+
+    private val _installSessions =
+        MutableStateFlow<List<PackageInstallSession>>(emptyList()).apply {
+            subscriptionCount
+                .map { count -> count > 0 }
+                .distinctUntilChanged()
+                // Drop initial false value
+                .dropWhile { !it }
+                .onEach { isActive ->
+                    if (isActive) {
+                        synchronized(sessions) {
+                            sessions.putAll(
+                                packageInstaller.allSessions
+                                    .map { session -> session.toModel() }
+                                    .associateBy { it.sessionId }
+                            )
+                            updateInstallerSessionsFlow()
+                        }
+                        packageInstaller.registerSessionCallback(
+                            this@PackageInstallerMonitor,
+                            bgHandler
+                        )
+                    } else {
+                        synchronized(sessions) {
+                            sessions.clear()
+                            updateInstallerSessionsFlow()
+                        }
+                        packageInstaller.unregisterSessionCallback(this@PackageInstallerMonitor)
+                    }
+                }
+                .launchIn(bgScope)
+        }
+
+    val installSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+        _installSessions.asStateFlow()
+
+    /** Called when a new installer session is created. */
+    override fun onCreated(sessionId: Int) {
+        logger.i({ "session created $int1" }) { int1 = sessionId }
+        updateSession(sessionId)
+    }
+
+    /** Called when new installer session has finished. */
+    override fun onFinished(sessionId: Int, success: Boolean) {
+        logger.i({ "session finished $int1" }) { int1 = sessionId }
+        synchronized(sessions) {
+            sessions.remove(sessionId)
+            updateInstallerSessionsFlow()
+        }
+    }
+
+    /**
+     * Badging details for the session changed. For example, the app icon or label has been updated.
+     */
+    override fun onBadgingChanged(sessionId: Int) {
+        logger.i({ "session badging changed $int1" }) { int1 = sessionId }
+        updateSession(sessionId)
+    }
+
+    /**
+     * A session is considered active when there is ongoing forward progress being made. For
+     * example, a package started downloading.
+     */
+    override fun onActiveChanged(sessionId: Int, active: Boolean) {
+        // Active status updates are not tracked for now
+    }
+
+    override fun onProgressChanged(sessionId: Int, progress: Float) {
+        // Progress updates are not tracked for now
+    }
+
+    private fun updateSession(sessionId: Int) {
+        val session = packageInstaller.getSessionInfo(sessionId)
+
+        synchronized(sessions) {
+            if (session == null) {
+                sessions.remove(sessionId)
+            } else {
+                sessions[sessionId] = session.toModel()
+            }
+            updateInstallerSessionsFlow()
+        }
+    }
+
+    @GuardedBy("sessions")
+    private fun updateInstallerSessionsFlow() {
+        _installSessions.value = sessions.values.toList()
+    }
+
+    companion object {
+        const val TAG = "PackageInstallerMonitor"
+
+        private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+            return PackageInstallSession(
+                sessionId = this.sessionId,
+                packageName = this.appPackageName,
+                icon = this.getAppIcon(),
+                user = this.user,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt
new file mode 100644
index 0000000..7025229
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.common.shared.model
+
+import android.graphics.Bitmap
+import android.os.UserHandle
+
+/** Represents a session of a package being installed on device. */
+data class PackageInstallSession(
+    val sessionId: Int,
+    val packageName: String,
+    val icon: Bitmap?,
+    val user: UserHandle,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index 9e7fb4e..153b7aa 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -20,6 +20,8 @@
 import android.app.DreamManager
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.communalHub
+import com.android.systemui.Flags.restartDreamOnUnocclude
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -27,8 +29,10 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
+import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 
@@ -43,6 +47,7 @@
     private val powerInteractor: PowerInteractor,
     private val keyguardInteractor: KeyguardInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val communalInteractor: CommunalInteractor,
     private val dreamManager: DreamManager,
     @Background private val bgScope: CoroutineScope,
 ) : CoreStartable {
@@ -52,6 +57,19 @@
             return
         }
 
+        // Return to dream from occluded when not already dreaming.
+        if (restartDreamOnUnocclude()) {
+            keyguardTransitionInteractor.startedKeyguardTransitionStep
+                .sample(keyguardInteractor.isDreaming, ::Pair)
+                .filter {
+                    it.first.from == KeyguardState.OCCLUDED &&
+                        it.first.to == KeyguardState.DREAMING &&
+                        !it.second
+                }
+                .onEach { dreamManager.startDream() }
+                .launchIn(bgScope)
+        }
+
         // Restart the dream underneath the hub in order to support the ability to swipe
         // away the hub to enter the dream.
         keyguardTransitionInteractor.finishedKeyguardState
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 1f54e70..fdb797d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -17,14 +17,13 @@
 package com.android.systemui.communal.data.repository
 
 import android.app.backup.BackupManager
-import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
 import android.content.ComponentName
 import android.os.UserHandle
-import androidx.annotation.WorkerThread
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.shared.model.PackageInstallSession
 import com.android.systemui.communal.data.backup.CommunalBackupUtils
-import com.android.systemui.communal.data.db.CommunalItemRank
 import com.android.systemui.communal.data.db.CommunalWidgetDao
-import com.android.systemui.communal.data.db.CommunalWidgetItem
 import com.android.systemui.communal.nano.CommunalHubState
 import com.android.systemui.communal.proto.toCommunalHubState
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -36,13 +35,15 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.util.kotlin.getValue
-import java.util.Optional
 import javax.inject.Inject
 import kotlin.coroutines.cancellation.CancellationException
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
@@ -88,7 +89,6 @@
 class CommunalWidgetRepositoryImpl
 @Inject
 constructor(
-    appWidgetManagerOptional: Optional<AppWidgetManager>,
     private val appWidgetHost: CommunalAppWidgetHost,
     @Background private val bgScope: CoroutineScope,
     @Background private val bgDispatcher: CoroutineDispatcher,
@@ -97,6 +97,7 @@
     @CommunalLog logBuffer: LogBuffer,
     private val backupManager: BackupManager,
     private val backupUtils: CommunalBackupUtils,
+    packageChangeRepository: PackageChangeRepository,
 ) : CommunalWidgetRepository {
     companion object {
         const val TAG = "CommunalWidgetRepository"
@@ -104,12 +105,39 @@
 
     private val logger = Logger(logBuffer, TAG)
 
-    private val appWidgetManager by appWidgetManagerOptional
+    /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */
+    private val widgetEntries: Flow<List<CommunalWidgetEntry>> =
+        combine(
+            communalWidgetDao.getWidgets(),
+            communalWidgetHost.appWidgetProviders,
+        ) { entries, providers ->
+            entries.mapNotNull { (rank, widget) ->
+                CommunalWidgetEntry(
+                    appWidgetId = widget.widgetId,
+                    componentName = widget.componentName,
+                    priority = rank.rank,
+                    providerInfo = providers[widget.widgetId]
+                )
+            }
+        }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
-        communalWidgetDao
-            .getWidgets()
-            .map { it.mapNotNull(::mapToContentModel) }
+        widgetEntries
+            .flatMapLatest { widgetEntries ->
+                // If and only if any widget is missing provider info, combine with the package
+                // installer sessions flow to check whether they are pending installation. This can
+                // happen after widgets are freshly restored from a backup. In most cases, provider
+                // info is available to all widgets, and is unnecessary to involve an API call to
+                // the package installer.
+                if (widgetEntries.any { it.providerInfo == null }) {
+                    packageChangeRepository.packageInstallSessionsForPrimaryUser.map { sessions ->
+                        widgetEntries.mapNotNull { entry -> mapToContentModel(entry, sessions) }
+                    }
+                } else {
+                    flowOf(widgetEntries.map(::mapToContentModel))
+                }
+            }
             // As this reads from a database and triggers IPCs to AppWidgetManager,
             // it should be executed in the background.
             .flowOn(bgDispatcher)
@@ -245,6 +273,9 @@
                 }
                 appWidgetHost.deleteAppWidgetId(widgetId)
             }
+
+            // Providers may have changed
+            communalWidgetHost.refreshProviders()
         }
     }
 
@@ -255,16 +286,57 @@
         }
     }
 
-    @WorkerThread
-    private fun mapToContentModel(
-        entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
-    ): CommunalWidgetContentModel? {
-        val (_, widgetId) = entry.value
-        val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null
-        return CommunalWidgetContentModel(
-            appWidgetId = widgetId,
-            providerInfo = providerInfo,
-            priority = entry.key.rank,
+    /**
+     * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with the assumption that the
+     * [AppWidgetProviderInfo] of the entry is available.
+     */
+    private fun mapToContentModel(entry: CommunalWidgetEntry): CommunalWidgetContentModel {
+        return CommunalWidgetContentModel.Available(
+            appWidgetId = entry.appWidgetId,
+            providerInfo = entry.providerInfo!!,
+            priority = entry.priority,
         )
     }
+
+    /**
+     * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with a list of install
+     * sessions. If the [AppWidgetProviderInfo] of the entry is absent, and its package is in the
+     * install sessions, the entry is mapped to a pending widget.
+     */
+    private fun mapToContentModel(
+        entry: CommunalWidgetEntry,
+        installSessions: List<PackageInstallSession>,
+    ): CommunalWidgetContentModel? {
+        if (entry.providerInfo != null) {
+            return CommunalWidgetContentModel.Available(
+                appWidgetId = entry.appWidgetId,
+                providerInfo = entry.providerInfo!!,
+                priority = entry.priority,
+            )
+        }
+
+        val session =
+            installSessions.firstOrNull {
+                it.packageName ==
+                    ComponentName.unflattenFromString(entry.componentName)?.packageName
+            }
+        return if (session != null) {
+            CommunalWidgetContentModel.Pending(
+                appWidgetId = entry.appWidgetId,
+                priority = entry.priority,
+                packageName = session.packageName,
+                icon = session.icon,
+                user = session.user,
+            )
+        } else {
+            null
+        }
+    }
+
+    private data class CommunalWidgetEntry(
+        val appWidgetId: Int,
+        val componentName: String,
+        val priority: Int,
+        var providerInfo: AppWidgetProviderInfo? = null,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 0042915..06c8396 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -60,9 +60,9 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.smartspace.data.repository.SmartspaceRepository
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.android.systemui.util.kotlin.emitOnStart
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
@@ -127,10 +127,10 @@
 
     /** Whether communal features are enabled and available. */
     val isCommunalAvailable: Flow<Boolean> =
-        and(
+        allOf(
                 communalSettingsInteractor.isCommunalEnabled,
                 not(keyguardInteractor.isEncryptedOrLockdown),
-                or(keyguardInteractor.isKeyguardShowing, keyguardInteractor.isDreaming)
+                anyOf(keyguardInteractor.isKeyguardShowing, keyguardInteractor.isDreaming)
             )
             .distinctUntilChanged()
             .onEach { available ->
@@ -163,6 +163,13 @@
                 initialValue = false,
             )
 
+    /** Whether to start dreaming when returning from occluded */
+    val dreamFromOccluded: Flow<Boolean> =
+        keyguardTransitionInteractor
+            .transitionStepsToState(KeyguardState.OCCLUDED)
+            .map { it.from == KeyguardState.DREAMING }
+            .stateIn(scope = applicationScope, SharingStarted.Eagerly, false)
+
     /**
      * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
      *
@@ -403,19 +410,30 @@
             updateOnWorkProfileBroadcastReceived,
         ) { widgets, allowedCategories, _ ->
             widgets.map { widget ->
-                if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
-                    // At least one category this widget specified is allowed, so show it
-                    WidgetContent.Widget(
-                        appWidgetId = widget.appWidgetId,
-                        providerInfo = widget.providerInfo,
-                        appWidgetHost = appWidgetHost,
-                        inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
-                    )
-                } else {
-                    WidgetContent.DisabledWidget(
-                        appWidgetId = widget.appWidgetId,
-                        providerInfo = widget.providerInfo,
-                    )
+                when (widget) {
+                    is CommunalWidgetContentModel.Available -> {
+                        if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
+                            // At least one category this widget specified is allowed, so show it
+                            WidgetContent.Widget(
+                                appWidgetId = widget.appWidgetId,
+                                providerInfo = widget.providerInfo,
+                                appWidgetHost = appWidgetHost,
+                                inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
+                            )
+                        } else {
+                            WidgetContent.DisabledWidget(
+                                appWidgetId = widget.appWidgetId,
+                                providerInfo = widget.providerInfo,
+                            )
+                        }
+                    }
+                    is CommunalWidgetContentModel.Pending -> {
+                        WidgetContent.PendingWidget(
+                            appWidgetId = widget.appWidgetId,
+                            packageName = widget.packageName,
+                            icon = widget.icon,
+                        )
+                    }
                 }
             }
         }
@@ -430,7 +448,15 @@
         } else {
             // Get associated work profile for the currently selected user.
             val workProfile = userTracker.userProfiles.find { it.isManagedProfile }
-            list.filter { it.providerInfo.profile.identifier != workProfile?.id }
+            list.filter { model ->
+                val uid =
+                    when (model) {
+                        is CommunalWidgetContentModel.Available ->
+                            model.providerInfo.profile.identifier
+                        is CommunalWidgetContentModel.Pending -> model.user.identifier
+                    }
+                uid != workProfile?.id
+            }
         }
 
     /** A flow of available smartspace targets. Currently only showing timers. */
@@ -513,7 +539,11 @@
     ): List<CommunalWidgetContentModel> {
         val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
         return list.filter { widget ->
-            currentUserIds.contains(widget.providerInfo.profile?.identifier)
+            when (widget) {
+                is CommunalWidgetContentModel.Available ->
+                    currentUserIds.contains(widget.providerInfo.profile?.identifier)
+                is CommunalWidgetContentModel.Pending -> true
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index 7061227..122240d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -19,6 +19,7 @@
 import android.appwidget.AppWidgetProviderInfo
 import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
 import android.content.pm.ApplicationInfo
+import android.graphics.Bitmap
 import android.widget.RemoteViews
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.widgets.CommunalAppWidgetHost
@@ -45,11 +46,10 @@
 
     sealed interface WidgetContent : CommunalContentModel {
         val appWidgetId: Int
-        val providerInfo: AppWidgetProviderInfo
 
         data class Widget(
             override val appWidgetId: Int,
-            override val providerInfo: AppWidgetProviderInfo,
+            val providerInfo: AppWidgetProviderInfo,
             val appWidgetHost: CommunalAppWidgetHost,
             val inQuietMode: Boolean,
         ) : WidgetContent {
@@ -66,7 +66,7 @@
 
         data class DisabledWidget(
             override val appWidgetId: Int,
-            override val providerInfo: AppWidgetProviderInfo
+            val providerInfo: AppWidgetProviderInfo
         ) : WidgetContent {
             override val key = KEY.disabledWidget(appWidgetId)
             // Widget size is always half.
@@ -75,6 +75,16 @@
             val appInfo: ApplicationInfo?
                 get() = providerInfo.providerInfo?.applicationInfo
         }
+
+        data class PendingWidget(
+            override val appWidgetId: Int,
+            val packageName: String,
+            val icon: Bitmap? = null,
+        ) : WidgetContent {
+            override val key = KEY.pendingWidget(appWidgetId)
+            // Widget size is always half.
+            override val size = CommunalContentSize.HALF
+        }
     }
 
     /** A placeholder item representing a new widget being added */
@@ -127,6 +137,10 @@
                 return "disabled_widget_$id"
             }
 
+            fun pendingWidget(id: Int): String {
+                return "pending_widget_$id"
+            }
+
             fun widgetPlaceholder(): String {
                 return "widget_placeholder_${UUID.randomUUID()}"
             }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index e141dc4..53aecc1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -17,10 +17,27 @@
 package com.android.systemui.communal.shared.model
 
 import android.appwidget.AppWidgetProviderInfo
+import android.graphics.Bitmap
+import android.os.UserHandle
 
 /** Encapsulates data for a communal widget. */
-data class CommunalWidgetContentModel(
-    val appWidgetId: Int,
-    val providerInfo: AppWidgetProviderInfo,
-    val priority: Int,
-)
+sealed interface CommunalWidgetContentModel {
+    val appWidgetId: Int
+    val priority: Int
+
+    /** Widget is ready to display */
+    data class Available(
+        override val appWidgetId: Int,
+        val providerInfo: AppWidgetProviderInfo,
+        override val priority: Int,
+    ) : CommunalWidgetContentModel
+
+    /** Widget is pending installation */
+    data class Pending(
+        override val appWidgetId: Int,
+        override val priority: Int,
+        val packageName: String,
+        val icon: Bitmap?,
+        val user: UserHandle,
+    ) : CommunalWidgetContentModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 3f92223..f6122ad 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -104,7 +104,12 @@
     ): Boolean =
         withContext(backgroundDispatcher) {
             val widgets = communalInteractor.widgetContent.first()
-            val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo }
+            val excludeList =
+                widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo(
+                    ArrayList()
+                ) {
+                    it.providerInfo
+                }
             getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
                 try {
                     activityLauncher.launch(it)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
index 337d873..9114aab 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
@@ -33,6 +33,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 
 /** View model for transitions related to the communal hub. */
@@ -49,6 +50,27 @@
     communalInteractor: CommunalInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
+    // Show UMO on glanceable hub immediately on transition into glanceable hub
+    private val showUmoFromOccludedToGlanceableHub: Flow<Boolean> =
+        keyguardTransitionInteractor
+            .transitionStepsFromState(KeyguardState.OCCLUDED)
+            .filter {
+                it.to == KeyguardState.GLANCEABLE_HUB &&
+                    (it.transitionState == TransitionState.STARTED ||
+                        it.transitionState == TransitionState.CANCELED)
+            }
+            .map { it.transitionState == TransitionState.STARTED }
+
+    private val showUmoFromGlanceableHubToOccluded: Flow<Boolean> =
+        keyguardTransitionInteractor
+            .transitionStepsFromState(KeyguardState.GLANCEABLE_HUB)
+            .filter {
+                it.to == KeyguardState.OCCLUDED &&
+                    (it.transitionState == TransitionState.FINISHED ||
+                        it.transitionState == TransitionState.CANCELED)
+            }
+            .map { it.transitionState != TransitionState.FINISHED }
+
     /**
      * Whether UMO location should be on communal. This flow is responsive to transitions so that a
      * new value is emitted at the right step of a transition to/from communal hub that the location
@@ -60,6 +82,8 @@
                 glanceableHubToLockscreenTransitionViewModel.showUmo,
                 dreamToGlanceableHubTransitionViewModel.showUmo,
                 glanceableHubToDreamTransitionViewModel.showUmo,
+                showUmoFromOccludedToGlanceableHub,
+                showUmoFromGlanceableHubToOccluded,
             )
             .distinctUntilChanged()
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 5f1d89e..b7e8205 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -24,6 +24,7 @@
 import android.widget.RemoteViews
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
+import javax.annotation.concurrent.GuardedBy
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
@@ -47,6 +48,8 @@
     /** App widget ids that have been removed and no longer available. */
     val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow()
 
+    @GuardedBy("observers") private val observers = mutableSetOf<Observer>()
+
     override fun onCreateView(
         context: Context,
         appWidgetId: Int,
@@ -77,6 +80,61 @@
         }
     }
 
+    override fun allocateAppWidgetId(): Int {
+        return super.allocateAppWidgetId().also { appWidgetId ->
+            backgroundScope.launch {
+                observers.forEach { observer -> observer.onAllocateAppWidgetId(appWidgetId) }
+            }
+        }
+    }
+
+    override fun deleteAppWidgetId(appWidgetId: Int) {
+        super.deleteAppWidgetId(appWidgetId)
+        backgroundScope.launch {
+            observers.forEach { observer -> observer.onDeleteAppWidgetId(appWidgetId) }
+        }
+    }
+
+    override fun startListening() {
+        super.startListening()
+        backgroundScope.launch { observers.forEach { observer -> observer.onHostStartListening() } }
+    }
+
+    override fun stopListening() {
+        super.stopListening()
+        backgroundScope.launch { observers.forEach { observer -> observer.onHostStopListening() } }
+    }
+
+    fun addObserver(observer: Observer) {
+        synchronized(observers) { observers.add(observer) }
+    }
+
+    fun removeObserver(observer: Observer) {
+        synchronized(observers) { observers.remove(observer) }
+    }
+
+    /**
+     * Allows another class to observe the [CommunalAppWidgetHost] and handle any logic there.
+     *
+     * This is mainly for testability as it is difficult to test a real instance of [AppWidgetHost]
+     * which communicates with framework services.
+     *
+     * Note: all the callbacks are launched from the background scope.
+     */
+    interface Observer {
+        /** Called immediately after the host has started listening for widget updates. */
+        fun onHostStartListening() {}
+
+        /** Called immediately after the host has stopped listening for widget updates. */
+        fun onHostStopListening() {}
+
+        /** Called immediately after a new app widget id has been allocated. */
+        fun onAllocateAppWidgetId(appWidgetId: Int) {}
+
+        /** Called immediately after an app widget id is to be deleted. */
+        fun onDeleteAppWidgetId(appWidgetId: Int) {}
+    }
+
     companion object {
         private const val TAG = "CommunalAppWidgetHost"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 8390d62..301da51 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -23,7 +23,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.settings.UserTracker
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
@@ -39,6 +39,7 @@
 @Inject
 constructor(
     private val appWidgetHost: CommunalAppWidgetHost,
+    private val communalWidgetHost: CommunalWidgetHost,
     private val communalInteractor: CommunalInteractor,
     private val userTracker: UserTracker,
     @Background private val bgScope: CoroutineScope,
@@ -46,7 +47,7 @@
 ) : CoreStartable {
 
     override fun start() {
-        or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+        anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
             // Only trigger updates on state changes, ignoring the initial false value.
             .pairwise(false)
             .filter { (previous, new) -> previous != new }
@@ -70,9 +71,11 @@
         // Always ensure this is called on the main/ui thread.
         withContext(uiDispatcher) {
             if (active) {
+                communalWidgetHost.startObservingHost()
                 appWidgetHost.startListening()
             } else {
                 appWidgetHost.stopListening()
+                communalWidgetHost.stopObservingHost()
             }
         }
 
@@ -83,7 +86,15 @@
     private fun validateWidgetsAndDeleteOrphaned(widgets: List<CommunalWidgetContentModel>) {
         val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
         widgets
-            .filter { widget -> !currentUserIds.contains(widget.providerInfo.profile?.identifier) }
+            .filter { widget ->
+                val uid =
+                    when (widget) {
+                        is CommunalWidgetContentModel.Available ->
+                            widget.providerInfo.profile?.identifier
+                        is CommunalWidgetContentModel.Pending -> widget.user.identifier
+                    }
+                !currentUserIds.contains(uid)
+            }
             .onEach { widget -> communalInteractor.deleteWidget(id = widget.appWidgetId) }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
index 93e2b37..42107c1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.communal.widgets
 
+import android.appwidget.AppWidgetHost.AppWidgetHostListener
 import android.appwidget.AppWidgetManager
 import android.appwidget.AppWidgetProviderInfo
 import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
@@ -23,6 +24,9 @@
 import android.content.ComponentName
 import android.os.Bundle
 import android.os.UserHandle
+import android.widget.RemoteViews
+import androidx.annotation.WorkerThread
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.Logger
 import com.android.systemui.log.dagger.CommunalLog
@@ -30,6 +34,11 @@
 import com.android.systemui.util.kotlin.getOrNull
 import java.util.Optional
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
 
 /**
  * Widget host that interacts with AppWidget service and host to bind and provide info for widgets
@@ -38,11 +47,12 @@
 class CommunalWidgetHost
 @Inject
 constructor(
+    @Background private val bgScope: CoroutineScope,
     private val appWidgetManager: Optional<AppWidgetManager>,
     private val appWidgetHost: CommunalAppWidgetHost,
     private val selectedUserInteractor: SelectedUserInteractor,
     @CommunalLog logBuffer: LogBuffer,
-) {
+) : CommunalAppWidgetHost.Observer {
     companion object {
         private const val TAG = "CommunalWidgetHost"
 
@@ -60,6 +70,19 @@
 
     private val logger = Logger(logBuffer, TAG)
 
+    private val _appWidgetProviders = MutableStateFlow(emptyMap<Int, AppWidgetProviderInfo?>())
+
+    /**
+     * A flow of mappings between an appWidgetId and its corresponding [AppWidgetProviderInfo].
+     * These [AppWidgetProviderInfo]s represent app widgets that are actively bound to the
+     * [CommunalAppWidgetHost].
+     *
+     * The [AppWidgetProviderInfo] may be null in the case that the widget is bound but its provider
+     * is unavailable. For example, its package is not installed.
+     */
+    val appWidgetProviders: StateFlow<Map<Int, AppWidgetProviderInfo?>> =
+        _appWidgetProviders.asStateFlow()
+
     /**
      * Allocate an app widget id and binds the widget with the provider and associated user.
      *
@@ -77,6 +100,7 @@
             )
         ) {
             logger.d("Successfully bound the widget $provider")
+            onProviderInfoUpdated(id, getAppWidgetInfo(id))
             return id
         }
         appWidgetHost.deleteAppWidgetId(id)
@@ -100,7 +124,83 @@
         return false
     }
 
+    @WorkerThread
     fun getAppWidgetInfo(widgetId: Int): AppWidgetProviderInfo? {
         return appWidgetManager.getOrNull()?.getAppWidgetInfo(widgetId)
     }
+
+    fun startObservingHost() {
+        appWidgetHost.addObserver(this@CommunalWidgetHost)
+    }
+
+    fun stopObservingHost() {
+        appWidgetHost.removeObserver(this@CommunalWidgetHost)
+    }
+
+    fun refreshProviders() {
+        bgScope.launch {
+            val newProviders = mutableMapOf<Int, AppWidgetProviderInfo?>()
+            appWidgetHost.appWidgetIds.forEach { appWidgetId ->
+                // Listen for updates from each bound widget
+                addListener(appWidgetId)
+
+                // Fetch provider info of the widget
+                newProviders[appWidgetId] = getAppWidgetInfo(appWidgetId)
+            }
+
+            _appWidgetProviders.value = newProviders.toMap()
+        }
+    }
+
+    override fun onHostStartListening() {
+        refreshProviders()
+    }
+
+    override fun onHostStopListening() {
+        // Remove listeners
+        _appWidgetProviders.value.keys.forEach { appWidgetId ->
+            appWidgetHost.removeListener(appWidgetId)
+        }
+
+        // Clear providers
+        _appWidgetProviders.value = emptyMap()
+    }
+
+    override fun onAllocateAppWidgetId(appWidgetId: Int) {
+        addListener(appWidgetId)
+    }
+
+    override fun onDeleteAppWidgetId(appWidgetId: Int) {
+        appWidgetHost.removeListener(appWidgetId)
+        _appWidgetProviders.value =
+            _appWidgetProviders.value.toMutableMap().also { it.remove(appWidgetId) }
+    }
+
+    private fun addListener(appWidgetId: Int) {
+        appWidgetHost.setListener(
+            appWidgetId,
+            CommunalAppWidgetHostListener(appWidgetId, this::onProviderInfoUpdated),
+        )
+    }
+
+    private fun onProviderInfoUpdated(appWidgetId: Int, providerInfo: AppWidgetProviderInfo?) {
+        bgScope.launch {
+            _appWidgetProviders.value =
+                _appWidgetProviders.value.toMutableMap().also { it[appWidgetId] = providerInfo }
+        }
+    }
+
+    /** A [AppWidgetHostListener] for [appWidgetId]. */
+    private class CommunalAppWidgetHostListener(
+        private val appWidgetId: Int,
+        private val onUpdateProviderInfo: (Int, AppWidgetProviderInfo?) -> Unit,
+    ) : AppWidgetHostListener {
+        override fun onUpdateProviderInfo(providerInfo: AppWidgetProviderInfo?) {
+            onUpdateProviderInfo(appWidgetId, providerInfo)
+        }
+
+        override fun onViewDataChanged(viewId: Int) {}
+
+        override fun updateAppWidget(remoteViews: RemoteViews?) {}
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
index aa6516d..2000f96 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
@@ -69,16 +69,18 @@
         @SysUISingleton
         @Provides
         fun provideCommunalWidgetHost(
+            @Application applicationScope: CoroutineScope,
             appWidgetManager: Optional<AppWidgetManager>,
             appWidgetHost: CommunalAppWidgetHost,
             selectedUserInteractor: SelectedUserInteractor,
             @CommunalLog logBuffer: LogBuffer,
         ): CommunalWidgetHost {
             return CommunalWidgetHost(
+                applicationScope,
                 appWidgetManager,
                 appWidgetHost,
                 selectedUserInteractor,
-                logBuffer
+                logBuffer,
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index ef3f10f..11e6f7a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -49,6 +49,7 @@
 import android.content.om.OverlayManager;
 import android.content.pm.IPackageManager;
 import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutManager;
 import android.content.res.AssetManager;
@@ -224,6 +225,13 @@
 
     @Provides
     @Singleton
+    static UserScopedService<ColorDisplayManager> provideScopedColorDisplayManager(
+            Context context) {
+        return new UserScopedServiceImpl<>(context, ColorDisplayManager.class);
+    }
+
+    @Provides
+    @Singleton
     static CrossWindowBlurListeners provideCrossWindowBlurListeners() {
         return CrossWindowBlurListeners.getInstance();
     }
@@ -483,6 +491,12 @@
 
     @Provides
     @Singleton
+    static PackageInstaller providePackageInstaller(PackageManager packageManager) {
+        return packageManager.getPackageInstaller();
+    }
+
+    @Provides
+    @Singleton
     static PackageManagerWrapper providePackageManagerWrapper() {
         return PackageManagerWrapper.getInstance();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 8c0a73c..6e04339 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -198,7 +198,6 @@
         mLowLightTransitionCoordinator = lowLightTransitionCoordinator;
 
         mBouncerlessScrimController = bouncerlessScrimController;
-        mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
 
         mKeyguardTransitionInteractor = keyguardTransitionInteractor;
 
@@ -234,6 +233,7 @@
         mJitterStartTimeMillis = System.currentTimeMillis();
         mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
         mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
+        mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
         final Region emptyRegion = Region.obtain();
         mView.getRootSurfaceControl().setTouchableRegion(emptyRegion);
         emptyRegion.recycle();
@@ -255,8 +255,9 @@
 
     @Override
     protected void onViewDetached() {
-        mHandler.removeCallbacks(this::updateBurnInOffsets);
+        mHandler.removeCallbacksAndMessages(null);
         mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
+        mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback);
 
         mDreamOverlayAnimationsController.cancelAnimations();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
index c6fb4f9..fc9406b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
@@ -19,12 +19,13 @@
 
 import com.android.systemui.keyboard.data.repository.KeyboardRepository
 import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl
+import com.android.systemui.keyboard.shortcut.ShortcutHelperModule
 import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository
 import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl
 import dagger.Binds
 import dagger.Module
 
-@Module
+@Module(includes = [ShortcutHelperModule::class])
 abstract class KeyboardModule {
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
new file mode 100644
index 0000000..5635f80
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
@@ -0,0 +1,69 @@
+/*
+ * 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
+
+import android.app.Activity
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.keyboardShortcutHelperRewrite
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import dagger.Binds
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface ShortcutHelperModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(ShortcutHelperActivity::class)
+    fun activity(impl: ShortcutHelperActivity): Activity
+
+    companion object {
+        @Provides
+        @IntoMap
+        @ClassKey(ShortcutHelperActivityStarter::class)
+        fun starter(implLazy: Lazy<ShortcutHelperActivityStarter>): CoreStartable {
+            return if (keyboardShortcutHelperRewrite()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                NoOpStartable
+            }
+        }
+
+        @Provides
+        @IntoMap
+        @ClassKey(ShortcutHelperRepository::class)
+        fun repo(implLazy: Lazy<ShortcutHelperRepository>): CoreStartable {
+            return if (keyboardShortcutHelperRewrite()) {
+                implLazy.get()
+            } else {
+                // No-op implementation when the flag is disabled.
+                NoOpStartable
+            }
+        }
+    }
+}
+
+private object NoOpStartable : CoreStartable {
+    override fun start() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
new file mode 100644
index 0000000..9450af4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Inactive
+import com.android.systemui.statusbar.CommandQueue
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class ShortcutHelperRepository
+@Inject
+constructor(
+    private val commandQueue: CommandQueue,
+    private val broadcastDispatcher: BroadcastDispatcher,
+) : CoreStartable {
+
+    val state = MutableStateFlow<ShortcutHelperState>(Inactive)
+
+    override fun start() {
+        registerBroadcastReceiver(
+            action = Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS,
+            onReceive = { state.value = Active() }
+        )
+        registerBroadcastReceiver(
+            action = Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS,
+            onReceive = { state.value = Inactive }
+        )
+        commandQueue.addCallback(
+            object : CommandQueue.Callbacks {
+                override fun dismissKeyboardShortcutsMenu() {
+                    state.value = Inactive
+                }
+
+                override fun toggleKeyboardShortcutsMenu(deviceId: Int) {
+                    state.value =
+                        if (state.value is Inactive) {
+                            Active(deviceId)
+                        } else {
+                            Inactive
+                        }
+                }
+            }
+        )
+    }
+
+    fun hide() {
+        state.value = Inactive
+    }
+
+    private fun registerBroadcastReceiver(action: String, onReceive: () -> Unit) {
+        broadcastDispatcher.registerReceiver(
+            receiver =
+                object : BroadcastReceiver() {
+                    override fun onReceive(context: Context, intent: Intent) {
+                        onReceive()
+                    }
+                },
+            filter = IntentFilter(action),
+            flags = Context.RECEIVER_EXPORTED or Context.RECEIVER_VISIBLE_TO_INSTANT_APPS
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt
new file mode 100644
index 0000000..d3f7e24
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class ShortcutHelperInteractor
+@Inject
+constructor(private val repository: ShortcutHelperRepository) {
+
+    val state: Flow<ShortcutHelperState> = repository.state
+
+    fun onUserLeave() {
+        repository.hide()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
new file mode 100644
index 0000000..d22d6c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.shared.model
+
+sealed interface ShortcutHelperState {
+    data object Inactive : ShortcutHelperState
+
+    data class Active(val deviceId: Int? = null) : ShortcutHelperState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
new file mode 100644
index 0000000..fbf52e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.ui
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class ShortcutHelperActivityStarter(
+    private val context: Context,
+    @Application private val applicationScope: CoroutineScope,
+    private val viewModel: ShortcutHelperViewModel,
+    private val startActivity: (Intent) -> Unit,
+) : CoreStartable {
+
+    @Inject
+    constructor(
+        context: Context,
+        @Application applicationScope: CoroutineScope,
+        viewModel: ShortcutHelperViewModel,
+    ) : this(
+        context,
+        applicationScope,
+        viewModel,
+        startActivity = { intent -> context.startActivity(intent) }
+    )
+
+    override fun start() {
+        applicationScope.launch {
+            viewModel.shouldShow.collect { shouldShow ->
+                if (shouldShow) {
+                    startShortcutHelperActivity()
+                }
+            }
+        }
+    }
+
+    private fun startShortcutHelperActivity() {
+        startActivity(
+            Intent(context, ShortcutHelperActivity::class.java)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
rename to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 692fbb0..934f9ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.keyboard.shortcut
+package com.android.systemui.keyboard.shortcut.ui.view
 
 import android.graphics.Insets
 import android.os.Bundle
@@ -24,16 +24,25 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.OnBackPressedCallback
 import androidx.core.view.updatePadding
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.res.R
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
 import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import javax.inject.Inject
+import kotlinx.coroutines.launch
 
 /**
  * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
  * small and large screen devices.
  */
-class ShortcutHelperActivity : ComponentActivity() {
+class ShortcutHelperActivity
+@Inject
+constructor(
+    private val viewModel: ShortcutHelperViewModel,
+) : ComponentActivity() {
 
     private val bottomSheetContainer
         get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
@@ -53,6 +62,24 @@
         setUpPredictiveBack()
         setUpSheetDismissListener()
         setUpDismissOnTouchOutside()
+        observeFinishRequired()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        if (isFinishing) {
+            viewModel.onUserLeave()
+        }
+    }
+
+    private fun observeFinishRequired() {
+        lifecycleScope.launch {
+            viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow ->
+                if (!shouldShow) {
+                    finish()
+                }
+            }
+        }
     }
 
     private fun setupEdgeToEdge() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
new file mode 100644
index 0000000..7e48c65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.ui.viewmodel
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+class ShortcutHelperViewModel
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val interactor: ShortcutHelperInteractor
+) {
+
+    val shouldShow =
+        interactor.state
+            .map { it is ShortcutHelperState.Active }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
+
+    fun onUserLeave() {
+        interactor.onUserLeave()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index a49b3ae..c11c49c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -19,6 +19,7 @@
 
 import android.os.Handler
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
@@ -57,21 +58,7 @@
         TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) }
     val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!)
     val refreshTransition = MutableSharedFlow<Config>(extraBufferCapacity = 1)
-    private var targetTransitionConfig: Config? = null
-
-    /**
-     * Emits the blueprint value to the collectors.
-     *
-     * @param blueprintId
-     * @return whether the transition has succeeded.
-     */
-    fun applyBlueprint(index: Int): Boolean {
-        ArrayList(blueprintIdMap.values)[index]?.let {
-            applyBlueprint(it)
-            return true
-        }
-        return false
-    }
+    @VisibleForTesting var targetTransitionConfig: Config? = null
 
     /**
      * Emits the blueprint value to the collectors.
@@ -81,27 +68,21 @@
      */
     fun applyBlueprint(blueprintId: String?): Boolean {
         val blueprint = blueprintIdMap[blueprintId]
-        return if (blueprint != null) {
-            applyBlueprint(blueprint)
-            true
-        } else {
+        if (blueprint == null) {
             Log.e(
                 TAG,
                 "Could not find blueprint with id: $blueprintId. " +
                     "Perhaps it was not added to KeyguardBlueprintModule?"
             )
-            false
+            return false
         }
-    }
 
-    /** Emits the blueprint value to the collectors. */
-    fun applyBlueprint(blueprint: KeyguardBlueprint?) {
         if (blueprint == this.blueprint.value) {
-            refreshBlueprint()
-            return
+            return true
         }
 
-        blueprint?.let { this.blueprint.value = it }
+        this.blueprint.value = blueprint
+        return true
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index e32bfcf..7f3274c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -134,7 +134,7 @@
             TransitionInfo(
                 ownerName = "",
                 from = KeyguardState.OFF,
-                to = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.OFF,
                 animator = null
             )
         )
@@ -266,6 +266,14 @@
     }
 
     override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+        _currentTransitionInfo.value =
+            TransitionInfo(
+                ownerName = "KeyguardTransitionRepository(boot)",
+                from = KeyguardState.OFF,
+                to = to,
+                animator = null
+            )
+
         emitTransition(
             TransitionStep(
                 KeyguardState.OFF,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 54d9a78..faab033 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.seconds
@@ -148,7 +148,7 @@
             }
         } else {
             scope.launch {
-                and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
+                allOf(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming))
                     .filterRelevantKeyguardStateAnd { isOccludedAndNotDreaming ->
                         isOccludedAndNotDreaming
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index e51ba83..2a7178f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -18,6 +18,7 @@
 
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
+import com.android.systemui.Flags.restartDreamOnUnocclude
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -90,12 +91,15 @@
                     .sample(
                         communalInteractor.isIdleOnCommunal,
                         communalInteractor.showCommunalFromOccluded,
+                        communalInteractor.dreamFromOccluded
                     )
-                    .collect { (_, isIdleOnCommunal, showCommunalFromOccluded) ->
+                    .collect { (_, isIdleOnCommunal, showCommunalFromOccluded, dreamFromOccluded) ->
                         // Occlusion signals come from the framework, and should interrupt any
                         // existing transition
                         val to =
-                            if (isIdleOnCommunal || showCommunalFromOccluded) {
+                            if (restartDreamOnUnocclude() && dreamFromOccluded) {
+                                KeyguardState.DREAMING
+                            } else if (isIdleOnCommunal || showCommunalFromOccluded) {
                                 KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
@@ -110,15 +114,19 @@
                         keyguardInteractor.isKeyguardShowing,
                         communalInteractor.isIdleOnCommunal,
                         communalInteractor.showCommunalFromOccluded,
+                        communalInteractor.dreamFromOccluded,
                     )
-                    .filterRelevantKeyguardStateAnd { (isOccluded, isShowing, _, _) ->
+                    .filterRelevantKeyguardStateAnd { (isOccluded, isShowing, _, _, _) ->
                         !isOccluded && isShowing
                     }
-                    .collect { (_, _, isIdleOnCommunal, showCommunalFromOccluded) ->
+                    .collect { (_, _, isIdleOnCommunal, showCommunalFromOccluded, dreamFromOccluded)
+                        ->
                         // Occlusion signals come from the framework, and should interrupt any
                         // existing transition
                         val to =
-                            if (isIdleOnCommunal || showCommunalFromOccluded) {
+                            if (restartDreamOnUnocclude() && dreamFromOccluded) {
+                                KeyguardState.DREAMING
+                            } else if (isIdleOnCommunal || showCommunalFromOccluded) {
                                 KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index da4f85e..857096e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -36,9 +36,12 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -64,12 +67,7 @@
 
     /** Current BlueprintId */
     val blueprintId =
-        combine(
-            configurationInteractor.onAnyConfigurationChange,
-            fingerprintPropertyInteractor.propertiesInitialized.filter { it },
-            clockInteractor.currentClock,
-            shadeInteractor.shadeMode,
-        ) { _, _, _, shadeMode ->
+        shadeInteractor.shadeMode.map { shadeMode ->
             val useSplitShade = shadeMode == ShadeMode.Split && !ComposeLockscreen.isEnabled
             when {
                 useSplitShade -> SplitShadeKeyguardBlueprint.ID
@@ -77,17 +75,29 @@
             }
         }
 
+    private val refreshEvents: Flow<Unit> =
+        merge(
+            configurationInteractor.onAnyConfigurationChange,
+            fingerprintPropertyInteractor.propertiesInitialized.filter { it }.map { Unit },
+        )
+
     init {
         applicationScope.launch { blueprintId.collect { transitionToBlueprint(it) } }
+        applicationScope.launch { refreshEvents.collect { refreshBlueprint() } }
     }
 
     /**
-     * Transitions to a blueprint.
+     * Transitions to a blueprint, or refreshes it if already applied.
      *
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun transitionToBlueprint(blueprintId: String): Boolean {
+    fun transitionOrRefreshBlueprint(blueprintId: String): Boolean {
+        if (blueprintId == blueprint.value.id) {
+            refreshBlueprint()
+            return true
+        }
+
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
@@ -97,7 +107,7 @@
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
-    fun transitionToBlueprint(blueprintId: Int): Boolean {
+    fun transitionToBlueprint(blueprintId: String): Boolean {
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 88367f4..2d7b737 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -55,7 +55,6 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -63,10 +62,10 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
@@ -179,12 +178,7 @@
                 isDreaming && isDozeOff(dozeTransitionModel.to)
             }
             .sample(powerInteractor.isAwake) { isAbleToDream, isAwake -> isAbleToDream && isAwake }
-            .flatMapLatest { isAbleToDream ->
-                flow {
-                    delay(50)
-                    emit(isAbleToDream)
-                }
-            }
+            .debounce(50L)
             .distinctUntilChanged()
 
     /** Whether the keyguard is showing or not. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 4f00495..e2b66c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -76,7 +76,7 @@
                         view,
                         HapticFeedbackConstants.CONFIRM,
                     )
-                    applicationScope.launch { viewModel.onLongPress() }
+                    applicationScope.launch { viewModel.onUserInteraction() }
                 }
             }
 
@@ -116,6 +116,17 @@
                 launch("$TAG#viewModel.accessibilityDelegateHint") {
                     viewModel.accessibilityDelegateHint.collect { hint ->
                         view.accessibilityHintType = hint
+                        if (hint != DeviceEntryIconView.AccessibilityHintType.NONE) {
+                            view.setOnClickListener {
+                                vibratorHelper.performHapticFeedback(
+                                    view,
+                                    HapticFeedbackConstants.CONFIRM,
+                                )
+                                applicationScope.launch { viewModel.onUserInteraction() }
+                            }
+                        } else {
+                            view.setOnClickListener(null)
+                        }
                     }
                 }
                 launch("$TAG#viewModel.useBackgroundProtection") {
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 ccc48b5..bda6438 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
@@ -36,7 +36,6 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
-import com.android.app.tracing.coroutines.launch
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
 import com.android.systemui.Flags.newAodTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index 35b2598..200d30c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -65,12 +65,12 @@
             object : AccessibilityDelegate() {
                 private val accessibilityAuthenticateHint =
                     AccessibilityNodeInfo.AccessibilityAction(
-                        AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+                        AccessibilityNodeInfoCompat.ACTION_CLICK,
                         resources.getString(R.string.accessibility_authenticate_hint)
                     )
                 private val accessibilityEnterHint =
                     AccessibilityNodeInfo.AccessibilityAction(
-                        AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+                        AccessibilityNodeInfoCompat.ACTION_CLICK,
                         resources.getString(R.string.accessibility_enter_hint)
                     )
                 override fun onInitializeAccessibilityNodeInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index ce7ec0e..962cdf1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -46,15 +46,14 @@
                 return
             }
 
-            if (
-                arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt())
-            ) {
-                pw.println("Transition succeeded!")
-            } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
-                pw.println("Transition succeeded!")
-            } else {
-                pw.println("Invalid argument! To see available blueprint ids, run:")
-                pw.println("$ adb shell cmd statusbar blueprint help")
+            when {
+                arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.")
+                keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) ->
+                    pw.println("Transition succeeded!")
+                else -> {
+                    pw.println("Invalid argument! To see available blueprint ids, run:")
+                    pw.println("$ adb shell cmd statusbar blueprint help")
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index 7c29b39..218967c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -38,6 +38,8 @@
 import com.google.android.material.math.MathUtils
 import kotlin.math.abs
 
+internal fun View.getRect(): Rect = Rect(this.left, this.top, this.right, this.bottom)
+
 internal fun View.setRect(rect: Rect) =
     this.setLeftTopRightBottom(rect.left, rect.top, rect.right, rect.bottom)
 
@@ -66,12 +68,18 @@
             val view = transition.view
             transition.values[PROP_VISIBILITY] = view.visibility
             transition.values[PROP_ALPHA] = view.alpha
-            transition.values[PROP_BOUNDS] = Rect(view.left, view.top, view.right, view.bottom)
+            transition.values[PROP_BOUNDS] = view.getRect()
 
             if (!captureSmartspace) return
-            val ss = (view.parent as View).findViewById<View>(sharedR.id.bc_smartspace_view)
-            if (ss == null) return
-            transition.values[SMARTSPACE_BOUNDS] = Rect(ss.left, ss.top, ss.right, ss.bottom)
+            val parent = view.parent as View
+            val targetSSView =
+                parent.findViewById<View>(sharedR.id.bc_smartspace_view)
+                    ?: parent.findViewById<View>(R.id.keyguard_slice_view)
+            if (targetSSView == null) {
+                Log.e(TAG, "Failed to find smartspace equivalent target for animation")
+                return
+            }
+            transition.values[SMARTSPACE_BOUNDS] = targetSSView.getRect()
         }
 
         open fun mutateBounds(
@@ -89,7 +97,13 @@
             startValues: TransitionValues?,
             endValues: TransitionValues?
         ): Animator? {
-            if (startValues == null || endValues == null) return null
+            if (startValues == null || endValues == null) {
+                Log.w(
+                    TAG,
+                    "Couldn't create animator: startValues=$startValues; endValues=$endValues"
+                )
+                return null
+            }
 
             var fromVis = startValues.values[PROP_VISIBILITY] as Int
             var fromIsVis = fromVis == View.VISIBLE
@@ -141,11 +155,12 @@
             fun assignAnimValues(src: String, fract: Float, vis: Int? = null) {
                 val bounds = computeBounds(fract)
                 val alpha = MathUtils.lerp(fromAlpha, toAlpha, fract)
-                if (DEBUG)
+                if (DEBUG) {
                     Log.i(
                         TAG,
                         "$src: $toView; fract=$fract; alpha=$alpha; vis=$vis; bounds=$bounds;"
                     )
+                }
                 toView.setVisibility(vis ?: View.VISIBLE)
                 toView.setAlpha(alpha)
                 toView.setRect(bounds)
@@ -245,12 +260,9 @@
             // Move normally if clock is not changing visibility
             if (fromIsVis == toIsVis) return
 
-            fromBounds.left = toBounds.left
-            fromBounds.right = toBounds.right
+            fromBounds.set(toBounds)
             if (viewModel.isLargeClockVisible.value) {
-                // Large clock shouldn't move
-                fromBounds.top = toBounds.top
-                fromBounds.bottom = toBounds.bottom
+                // Large clock shouldn't move; fromBounds already set
             } else if (toSSBounds != null && fromSSBounds != null) {
                 // Instead of moving the small clock the full distance, we compute the distance
                 // smartspace will move. We then scale this to match the duration of this animation
@@ -306,12 +318,9 @@
             // Move normally if clock is not changing visibility
             if (fromIsVis == toIsVis) return
 
-            toBounds.left = fromBounds.left
-            toBounds.right = fromBounds.right
+            toBounds.set(fromBounds)
             if (!viewModel.isLargeClockVisible.value) {
-                // Large clock shouldn't move
-                toBounds.top = fromBounds.top
-                toBounds.bottom = fromBounds.bottom
+                // Large clock shouldn't move; toBounds already set
             } else if (toSSBounds != null && fromSSBounds != null) {
                 // Instead of moving the small clock the full distance, we compute the distance
                 // smartspace will move. We then scale this to match the duration of this animation
@@ -321,7 +330,7 @@
                 toBounds.top = fromBounds.top - ssTranslation
                 toBounds.bottom = fromBounds.bottom - ssTranslation
             } else {
-                Log.w(TAG, "mutateBounds: smallClock received no smartspace bounds")
+                Log.e(TAG, "mutateBounds: smallClock received no smartspace bounds")
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index da2fcc4..53b2697 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -19,6 +19,7 @@
 import android.animation.FloatEvaluator
 import android.animation.IntEvaluator
 import com.android.keyguard.KeyguardViewController
+import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -68,6 +69,7 @@
     private val keyguardViewController: Lazy<KeyguardViewController>,
     private val deviceEntryInteractor: DeviceEntryInteractor,
     private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
+    private val accessibilityInteractor: AccessibilityInteractor,
     @Application private val scope: CoroutineScope,
 ) {
     val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
@@ -232,7 +234,8 @@
             }
         }
     val isVisible: Flow<Boolean> = deviceEntryViewAlpha.map { it > 0f }.distinctUntilChanged()
-    val isLongPressEnabled: Flow<Boolean> =
+
+    private val isInteractive: Flow<Boolean> =
         combine(
             iconType,
             isUdfpsSupported,
@@ -244,17 +247,24 @@
                 DeviceEntryIconView.IconType.NONE -> false
             }
         }
-
     val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> =
-        combine(iconType, isLongPressEnabled) { deviceEntryStatus, longPressEnabled ->
-            if (longPressEnabled) {
-                deviceEntryStatus.toAccessibilityHintType()
+        accessibilityInteractor.isEnabled.flatMapLatest { touchExplorationEnabled ->
+            if (touchExplorationEnabled) {
+                combine(iconType, isInteractive) { iconType, isInteractive ->
+                    if (isInteractive) {
+                        iconType.toAccessibilityHintType()
+                    } else {
+                        DeviceEntryIconView.AccessibilityHintType.NONE
+                    }
+                }
             } else {
-                DeviceEntryIconView.AccessibilityHintType.NONE
+                flowOf(DeviceEntryIconView.AccessibilityHintType.NONE)
             }
         }
 
-    suspend fun onLongPress() {
+    val isLongPressEnabled: Flow<Boolean> = isInteractive
+
+    suspend fun onUserInteraction() {
         if (SceneContainerFlag.isEnabled) {
             deviceEntryInteractor.attemptDeviceEntry()
         } else {
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 24a7c51..bbcea56 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
@@ -42,7 +42,7 @@
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.ui.AnimatableEvent
@@ -64,7 +64,6 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
@@ -134,7 +133,7 @@
     private val isOnLockscreen: Flow<Boolean> =
         combine(
                 keyguardTransitionInteractor.isFinishedInState(LOCKSCREEN).onStart { emit(false) },
-                or(
+                anyOf(
                     keyguardTransitionInteractor.isInTransitionToState(LOCKSCREEN),
                     keyguardTransitionInteractor.isInTransitionFromState(LOCKSCREEN),
                 ),
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 933065b..295b293 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -442,7 +442,7 @@
                                         | PackageManager.MATCH_DISABLED_COMPONENTS
                                         | PackageManager.GET_SHARED_LIBRARY_FILES));
                 int resId = resources.getIdentifier(
-                        "gesture_blocking_activities", "array", recentsPackageName);
+                        "back_gesture_blocking_activities", "array", recentsPackageName);
 
                 if (resId == 0) {
                     Log.e(TAG, "No resource found for gesture-blocking activities");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
index 6fb5174..5720f76 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
@@ -125,7 +125,10 @@
 
     public void setNumPages(int numPages) {
         setVisibility(numPages > 1 ? View.VISIBLE : View.GONE);
-        if (numPages == getChildCount()) {
+        int childCount = getChildCount();
+        // We're checking if the width needs to be updated as it's possible that the number of pages
+        // was changed while the page indicator was not visible, automatically skipping onMeasure.
+        if (numPages == childCount && calculateWidth(childCount) == getMeasuredWidth()) {
             return;
         }
         if (mAnimating) {
@@ -295,6 +298,10 @@
         }
     }
 
+    private int calculateWidth(int numPages) {
+        return (mPageIndicatorWidth - mPageDotWidth) * (numPages - 1) + mPageDotWidth;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         final int N = getChildCount();
@@ -309,7 +316,7 @@
         for (int i = 0; i < N; i++) {
             getChildAt(i).measure(widthChildSpec, heightChildSpec);
         }
-        int width = (mPageIndicatorWidth - mPageDotWidth) * (N - 1) + mPageDotWidth;
+        int width = calculateWidth(N);
         setMeasuredDimension(width, mPageIndicatorHeight);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index b705a03..ea89be6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.os.Handler;
 
+import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogModule;
 import com.android.systemui.dagger.NightDisplayListenerModule;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -60,6 +61,7 @@
  */
 @Module(subcomponents = {QSFragmentComponent.class, QSSceneComponent.class},
         includes = {
+                BluetoothTileDialogModule.class,
                 MediaModule.class,
                 PanelsModule.class,
                 QSExternalModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index b515ce0..278352c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel.DEBUG
 import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
 import com.android.systemui.log.core.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.QSConfigLog
 import com.android.systemui.log.dagger.QSLog
@@ -56,6 +57,9 @@
     fun d(@CompileTimeConstant msg: String, arg: Any) {
         buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
     }
+    fun i(@CompileTimeConstant msg: String, arg: Any) {
+        buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
+    }
 
     fun logTileAdded(tileSpec: String) {
         buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" })
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
new file mode 100644
index 0000000..28c1fbf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class IconAndNameCustomRepository
+@Inject
+constructor(
+    private val installedTilesComponentRepository: InstalledTilesComponentRepository,
+    private val userTracker: UserTracker,
+    @Background private val backgroundContext: CoroutineContext,
+) {
+    /**
+     * Returns a list of the icon/labels for all available (installed and enabled) tile services.
+     *
+     * No order is guaranteed.
+     */
+    suspend fun getCustomTileData(): List<EditTileData> {
+        return withContext(backgroundContext) {
+            val installedTiles =
+                installedTilesComponentRepository.getInstalledTilesServiceInfos(userTracker.userId)
+            val packageManager = userTracker.userContext.packageManager
+            installedTiles
+                .map {
+                    val tileSpec = TileSpec.create(it.componentName)
+                    val label = it.loadLabel(packageManager)
+                    val icon = it.loadIcon(packageManager)
+                    val appName = it.applicationInfo.loadLabel(packageManager)
+                    if (icon != null) {
+                        EditTileData(
+                            tileSpec,
+                            Icon.Loaded(icon, ContentDescription.Loaded(label.toString())),
+                            Text.Loaded(label.toString()),
+                            Text.Loaded(appName.toString()),
+                        )
+                    } else {
+                        null
+                    }
+                }
+                .filterNotNull()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
new file mode 100644
index 0000000..ec9d151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+@SysUISingleton
+class StockTilesRepository
+@Inject
+constructor(
+    @Main private val resources: Resources,
+) {
+    /**
+     * List of stock platform tiles. All of the specs will be of type [TileSpec.PlatformTileSpec].
+     */
+    val stockTiles =
+        resources
+            .getString(R.string.quick_settings_tiles_stock)
+            .split(",")
+            .map(TileSpec::create)
+            .filterNot { it is TileSpec.Invalid }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
new file mode 100644
index 0000000..3b29422
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.data.repository.IconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.StockTilesRepository
+import com.android.systemui.qs.panels.domain.model.EditTilesModel
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import javax.inject.Inject
+
+@SysUISingleton
+class EditTilesListInteractor
+@Inject
+constructor(
+    private val stockTilesRepository: StockTilesRepository,
+    private val qsTileConfigProvider: QSTileConfigProvider,
+    private val iconAndNameCustomRepository: IconAndNameCustomRepository,
+) {
+    /**
+     * Provides a list of the tiles to edit, with their UI information (icon, labels).
+     *
+     * The icons have the label as their content description.
+     */
+    suspend fun getTilesToEdit(): EditTilesModel {
+        val stockTiles =
+            stockTilesRepository.stockTiles.map {
+                if (qsTileConfigProvider.hasConfig(it.spec)) {
+                    val config = qsTileConfigProvider.getConfig(it.spec)
+                    EditTileData(
+                        it,
+                        Icon.Resource(
+                            config.uiConfig.iconRes,
+                            ContentDescription.Resource(config.uiConfig.labelRes)
+                        ),
+                        Text.Resource(config.uiConfig.labelRes),
+                        null,
+                    )
+                } else {
+                    EditTileData(
+                        it,
+                        Icon.Resource(
+                            android.R.drawable.star_on,
+                            ContentDescription.Loaded(it.spec)
+                        ),
+                        Text.Loaded(it.spec),
+                        null
+                    )
+                }
+            }
+        return EditTilesModel(stockTiles, iconAndNameCustomRepository.getCustomTileData())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt
new file mode 100644
index 0000000..b573b9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.model
+
+import com.android.systemui.qs.panels.shared.model.EditTileData
+
+data class EditTilesModel(
+    val stockTiles: List<EditTileData>,
+    val customTiles: List<EditTileData>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
new file mode 100644
index 0000000..8b70bb9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.shared.model
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+data class EditTileData(
+    val tileSpec: TileSpec,
+    val icon: Icon,
+    val label: Text,
+    val appName: Text?,
+) {
+    init {
+        check(
+            (tileSpec is TileSpec.PlatformTileSpec && appName == null) ||
+                (tileSpec is TileSpec.CustomTileSpec && appName != null)
+        ) {
+            "tileSpec: $tileSpec - appName: $appName. " +
+                "appName must be non-null for custom tiles and only for custom tiles."
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
new file mode 100644
index 0000000..5c17fd1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.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.qs.panels.ui.compose
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
+
+@Composable
+fun EditMode(
+    viewModel: EditModeViewModel,
+    modifier: Modifier = Modifier,
+) {
+    val gridLayout by viewModel.gridLayout.collectAsState()
+    val tiles by viewModel.tiles.collectAsState(emptyList())
+
+    BackHandler { viewModel.stopEditing() }
+
+    DisposableEffect(Unit) { onDispose { viewModel.stopEditing() } }
+
+    Column(modifier) {
+        gridLayout.EditTileGrid(
+            tiles,
+            Modifier,
+            viewModel::addTile,
+            viewModel::removeTile,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 68ce5d8..8806931 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,7 +18,9 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
 
 interface GridLayout {
     @Composable
@@ -26,4 +28,12 @@
         tiles: List<TileViewModel>,
         modifier: Modifier,
     )
+
+    @Composable
+    fun EditTileGrid(
+        tiles: List<EditTileViewModel>,
+        modifier: Modifier,
+        onAddTile: (TileSpec, Int) -> Unit,
+        onRemoveTile: (TileSpec) -> Unit,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index e2143e0..86cc6f5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -23,11 +23,15 @@
 import androidx.compose.animation.graphics.res.animatedVectorResource
 import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
 import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
@@ -37,8 +41,14 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridScope
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Icon
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
@@ -47,6 +57,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -56,14 +67,28 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.res.integerResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.Expandable
 import com.android.compose.theme.colorAttr
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.load
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.ActiveTileColorAttributes
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileColorAttributes
 import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.toUiState
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.res.R
 import javax.inject.Inject
@@ -75,6 +100,8 @@
 class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: IconTilesInteractor) :
     GridLayout {
 
+    private object TileType
+
     @Composable
     override fun TileGrid(
         tiles: List<TileViewModel>,
@@ -88,17 +115,7 @@
         val iconTilesSpecs by
             iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet())
 
-        LazyVerticalGrid(
-            columns =
-                GridCells.Fixed(
-                    integerResource(R.integer.quick_settings_infinite_grid_num_columns)
-                ),
-            verticalArrangement =
-                Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
-            horizontalArrangement =
-                Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
-            modifier = modifier
-        ) {
+        TileLazyGrid(modifier) {
             items(
                 tiles.size,
                 span = { index ->
@@ -119,7 +136,7 @@
         }
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class)
+    @OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
     @Composable
     private fun Tile(
         tile: TileViewModel,
@@ -131,96 +148,302 @@
                 .mapLatest { it.toUiState() }
                 .collectAsState(initial = tile.currentState.toUiState())
         val context = LocalContext.current
-        val horizontalAlignment =
-            if (iconOnly) {
-                Alignment.CenterHorizontally
-            } else {
-                Alignment.Start
-            }
 
-        Row(
-            modifier =
-                modifier
-                    .fillMaxWidth()
-                    .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
-                    .clickable { tile.onClick(null) }
-                    .background(colorAttr(state.colors.background))
-                    .padding(
-                        horizontal = dimensionResource(id = R.dimen.qs_label_container_margin)
-                    ),
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalArrangement =
-                Arrangement.spacedBy(
-                    space = dimensionResource(id = R.dimen.qs_label_container_margin),
-                    alignment = horizontalAlignment
-                )
+        Expandable(
+            color = colorAttr(state.colors.background),
+            shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)),
         ) {
-            val icon =
-                remember(state.icon) {
-                    state.icon.get().let {
-                        if (it is QSTileImpl.ResourceIcon) {
-                            Icon.Resource(it.resId, null)
-                        } else {
-                            Icon.Loaded(it.getDrawable(context), null)
+            Row(
+                modifier =
+                    modifier
+                        .combinedClickable(
+                            onClick = { tile.onClick(it) },
+                            onLongClick = { tile.onLongClick(it) }
+                        )
+                        .tileModifier(state.colors),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement = tileHorizontalArrangement(iconOnly),
+            ) {
+                val icon =
+                    remember(state.icon) {
+                        state.icon.get().let {
+                            if (it is QSTileImpl.ResourceIcon) {
+                                Icon.Resource(it.resId, null)
+                            } else {
+                                Icon.Loaded(it.getDrawable(context), null)
+                            }
                         }
                     }
-                }
-            TileIcon(icon, colorAttr(state.colors.icon))
+                TileContent(
+                    label = state.label.toString(),
+                    secondaryLabel = state.secondaryLabel?.toString(),
+                    icon = icon,
+                    colors = state.colors,
+                    iconOnly = iconOnly
+                )
+            }
+        }
+    }
 
-            if (!iconOnly) {
-                Column(
-                    verticalArrangement = Arrangement.Center,
-                    modifier = Modifier.fillMaxHeight()
-                ) {
-                    Text(
-                        state.label.toString(),
-                        color = colorAttr(state.colors.label),
-                        modifier = Modifier.basicMarquee(),
-                    )
-                    if (!TextUtils.isEmpty(state.secondaryLabel)) {
-                        Text(
-                            state.secondaryLabel.toString(),
-                            color = colorAttr(state.colors.secondaryLabel),
-                            modifier = Modifier.basicMarquee(),
-                        )
-                    }
+    @Composable
+    override fun EditTileGrid(
+        tiles: List<EditTileViewModel>,
+        modifier: Modifier,
+        onAddTile: (TileSpec, Int) -> Unit,
+        onRemoveTile: (TileSpec) -> Unit,
+    ) {
+        val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
+        val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
+        val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
+            onAddTile(it, POSITION_AT_END)
+        }
+        val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet())
+        val isIconOnly: (TileSpec) -> Boolean =
+            remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
+
+        TileLazyGrid(modifier = modifier) {
+            // These Text are just placeholders to see the different sections. Not final UI.
+            item(span = { GridItemSpan(maxLineSpan) }) {
+                Text("Current tiles", color = Color.White)
+            }
+
+            editTiles(
+                currentTiles,
+                ClickAction.REMOVE,
+                onRemoveTile,
+                isIconOnly,
+                indicatePosition = true,
+            )
+
+            item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
+
+            editTiles(
+                otherTilesStock,
+                ClickAction.ADD,
+                addTileToEnd,
+                isIconOnly,
+            )
+
+            item(span = { GridItemSpan(maxLineSpan) }) {
+                Text("Custom tiles to add", color = Color.White)
+            }
+
+            editTiles(
+                otherTilesCustom,
+                ClickAction.ADD,
+                addTileToEnd,
+                isIconOnly,
+            )
+        }
+    }
+
+    private fun LazyGridScope.editTiles(
+        tiles: List<EditTileViewModel>,
+        clickAction: ClickAction,
+        onClick: (TileSpec) -> Unit,
+        isIconOnly: (TileSpec) -> Boolean,
+        indicatePosition: Boolean = false,
+    ) {
+        items(
+            count = tiles.size,
+            key = { tiles[it].tileSpec.spec },
+            span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
+            contentType = { TileType }
+        ) {
+            val viewModel = tiles[it]
+            val canClick =
+                when (clickAction) {
+                    ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
+                    ClickAction.REMOVE ->
+                        AvailableEditActions.REMOVE in viewModel.availableEditActions
+                }
+            val onClickActionName =
+                when (clickAction) {
+                    ClickAction.ADD ->
+                        stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+                    ClickAction.REMOVE ->
+                        stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+                }
+            val stateDescription =
+                if (indicatePosition) {
+                    stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
+                } else {
+                    ""
+                }
+
+            Box(
+                modifier =
+                    Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) }
+                        .animateItem()
+                        .semantics {
+                            onClick(onClickActionName) { false }
+                            this.stateDescription = stateDescription
+                        }
+            ) {
+                EditTile(
+                    tileViewModel = viewModel,
+                    isIconOnly(viewModel.tileSpec),
+                    modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+                )
+                if (canClick) {
+                    Badge(clickAction, Modifier.align(Alignment.TopEnd))
                 }
             }
         }
     }
 
-    @OptIn(ExperimentalAnimationGraphicsApi::class)
     @Composable
-    private fun TileIcon(icon: Icon, color: Color) {
-        val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
-        val context = LocalContext.current
-        val loadedDrawable =
-            remember(icon, context) {
-                when (icon) {
-                    is Icon.Loaded -> icon.drawable
-                    is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
-                }
-            }
-        if (loadedDrawable !is Animatable) {
+    private fun Badge(action: ClickAction, modifier: Modifier = Modifier) {
+        Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) {
             Icon(
-                icon = icon,
-                tint = color,
-                modifier = modifier,
+                imageVector =
+                    when (action) {
+                        ClickAction.ADD -> Icons.Filled.Add
+                        ClickAction.REMOVE -> Icons.Filled.Remove
+                    },
+                "",
+                tint = Color.Black,
             )
-        } else if (icon is Icon.Resource) {
-            val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
-            var atEnd by remember(icon.res) { mutableStateOf(false) }
-            LaunchedEffect(key1 = icon.res) {
-                delay(350)
-                atEnd = true
+        }
+    }
+
+    @Composable
+    private fun EditTile(
+        tileViewModel: EditTileViewModel,
+        iconOnly: Boolean,
+        modifier: Modifier = Modifier,
+    ) {
+        val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
+        val colors = ActiveTileColorAttributes
+
+        Row(
+            modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label },
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = tileHorizontalArrangement(iconOnly)
+        ) {
+            TileContent(
+                label = label,
+                secondaryLabel = tileViewModel.appName?.load(),
+                colors = colors,
+                icon = tileViewModel.icon,
+                iconOnly = iconOnly,
+                animateIconToEnd = true,
+            )
+        }
+    }
+
+    private enum class ClickAction {
+        ADD,
+        REMOVE,
+    }
+}
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+private fun TileIcon(
+    icon: Icon,
+    color: Color,
+    animateToEnd: Boolean = false,
+) {
+    val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
+    val context = LocalContext.current
+    val loadedDrawable =
+        remember(icon, context) {
+            when (icon) {
+                is Icon.Loaded -> icon.drawable
+                is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
             }
-            val painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
-            Image(
-                painter = painter,
-                contentDescription = null,
-                colorFilter = ColorFilter.tint(color = color),
-                modifier = modifier
+        }
+    if (loadedDrawable !is Animatable) {
+        Icon(
+            icon = icon,
+            tint = color,
+            modifier = modifier,
+        )
+    } else if (icon is Icon.Resource) {
+        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+        val painter =
+            if (animateToEnd) {
+                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+            } else {
+                var atEnd by remember(icon.res) { mutableStateOf(false) }
+                LaunchedEffect(key1 = icon.res) {
+                    delay(350)
+                    atEnd = true
+                }
+                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+            }
+        Image(
+            painter = painter,
+            contentDescription = null,
+            colorFilter = ColorFilter.tint(color = color),
+            modifier = modifier
+        )
+    }
+}
+
+@Composable
+private fun TileLazyGrid(
+    modifier: Modifier = Modifier,
+    content: LazyGridScope.() -> Unit,
+) {
+    LazyVerticalGrid(
+        columns =
+            GridCells.Fixed(integerResource(R.integer.quick_settings_infinite_grid_num_columns)),
+        verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
+        horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
+        modifier = modifier,
+        content = content,
+    )
+}
+
+@Composable
+private fun Modifier.tileModifier(colors: TileColorAttributes): Modifier {
+    return fillMaxWidth()
+        .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
+        .background(colorAttr(colors.background))
+        .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin))
+}
+
+@Composable
+private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal {
+    val horizontalAlignment =
+        if (iconOnly) {
+            Alignment.CenterHorizontally
+        } else {
+            Alignment.Start
+        }
+    return spacedBy(
+        space = dimensionResource(id = R.dimen.qs_label_container_margin),
+        alignment = horizontalAlignment
+    )
+}
+
+@Composable
+private fun TileContent(
+    label: String,
+    secondaryLabel: String?,
+    icon: Icon,
+    colors: TileColorAttributes,
+    iconOnly: Boolean,
+    animateIconToEnd: Boolean = false,
+) {
+    TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
+
+    if (!iconOnly) {
+        Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+            Text(
+                label,
+                color = colorAttr(colors.label),
+                modifier = Modifier.basicMarquee(),
             )
+            if (!TextUtils.isEmpty(secondaryLabel)) {
+                Text(
+                    secondaryLabel ?: "",
+                    color = colorAttr(colors.secondaryLabel),
+                    modifier = Modifier.basicMarquee(),
+                )
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
new file mode 100644
index 0000000..69f50a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.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.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.GridLayoutTypeInteractor
+import com.android.systemui.qs.panels.shared.model.GridLayoutType
+import com.android.systemui.qs.panels.ui.compose.GridLayout
+import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+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.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class EditModeViewModel
+@Inject
+constructor(
+    private val editTilesListInteractor: EditTilesListInteractor,
+    private val currentTilesInteractor: CurrentTilesInteractor,
+    private val minTilesInteractor: MinimumTilesInteractor,
+    private val defaultGridLayout: InfiniteGridLayout,
+    @Application private val applicationScope: CoroutineScope,
+    gridLayoutTypeInteractor: GridLayoutTypeInteractor,
+    gridLayoutMap: Map<GridLayoutType, @JvmSuppressWildcards GridLayout>,
+) {
+    private val _isEditing = MutableStateFlow(false)
+
+    /**
+     * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this
+     */
+    val isEditing = _isEditing.asStateFlow()
+    private val minimumTiles: Int
+        get() = minTilesInteractor.minNumberOfTiles
+
+    val gridLayout: StateFlow<GridLayout> =
+        gridLayoutTypeInteractor.layout
+            .map { gridLayoutMap[it] ?: defaultGridLayout }
+            .stateIn(
+                applicationScope,
+                SharingStarted.WhileSubscribed(),
+                defaultGridLayout,
+            )
+
+    /**
+     * Flow of view models for each tile that should be visible in edit mode (or empty flow when not
+     * editing).
+     *
+     * Guarantees of the data:
+     * * The data for the tiles is fetched once whenever [isEditing] goes from `false` to `true`.
+     *   This prevents icons/labels changing while in edit mode.
+     * * It tracks the current tiles as they are added/removed/moved by the user.
+     * * The tiles that are current will be in the same relative order as the user sees them in
+     *   Quick Settings.
+     * * The tiles that are not current will preserve their relative order even when the current
+     *   tiles change.
+     */
+    val tiles =
+        isEditing.flatMapLatest {
+            if (it) {
+                val editTilesData = editTilesListInteractor.getTilesToEdit()
+                currentTilesInteractor.currentTiles.map { tiles ->
+                    val currentSpecs = tiles.map { it.spec }
+                    val canRemoveTiles = currentSpecs.size > minimumTiles
+                    val allTiles = editTilesData.stockTiles + editTilesData.customTiles
+                    val allTilesMap = allTiles.associate { it.tileSpec to it }
+                    val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
+                    val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
+
+                    (currentTiles + nonCurrentTiles).map {
+                        val current = it.tileSpec in currentSpecs
+                        val availableActions = buildSet {
+                            if (current) {
+                                add(AvailableEditActions.MOVE)
+                                if (canRemoveTiles) {
+                                    add(AvailableEditActions.REMOVE)
+                                }
+                            } else {
+                                add(AvailableEditActions.ADD)
+                            }
+                        }
+                        EditTileViewModel(
+                            it.tileSpec,
+                            it.icon,
+                            it.label,
+                            it.appName,
+                            current,
+                            availableActions
+                        )
+                    }
+                }
+            } else {
+                emptyFlow()
+            }
+        }
+
+    /** @see isEditing */
+    fun startEditing() {
+        _isEditing.value = true
+    }
+
+    /** @see isEditing */
+    fun stopEditing() {
+        _isEditing.value = false
+    }
+
+    /** Immediately moves [tileSpec] to [position]. */
+    fun moveTile(tileSpec: TileSpec, position: Int) {
+        throw NotImplementedError("This is not supported yet")
+    }
+
+    /** Immediately adds [tileSpec] to the current tiles at [position]. */
+    fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
+        currentTilesInteractor.addTile(tileSpec, position)
+    }
+
+    /** Immediately removes [tileSpec] from the current tiles. */
+    fun removeTile(tileSpec: TileSpec) {
+        currentTilesInteractor.removeTiles(listOf(tileSpec))
+    }
+
+    /** Immediately resets the current tiles to the default list. */
+    fun resetCurrentTilesToDefault() {
+        throw NotImplementedError("This is not supported yet")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
new file mode 100644
index 0000000..ba9a044
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/**
+ * View model for each tile that is available to be added/removed/moved in Edit mode.
+ *
+ * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in
+ * Quick Settings.
+ */
+class EditTileViewModel(
+    val tileSpec: TileSpec,
+    val icon: Icon,
+    val label: Text,
+    val appName: Text?,
+    val isCurrent: Boolean,
+    val availableEditActions: Set<AvailableEditActions>,
+)
+
+enum class AvailableEditActions {
+    ADD,
+    REMOVE,
+    MOVE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index cfcea98..c5b2737 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -23,6 +23,7 @@
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ServiceInfo
 import android.os.UserHandle
 import android.service.quicksettings.TileService
 import androidx.annotation.GuardedBy
@@ -36,14 +37,17 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
 
 interface InstalledTilesComponentRepository {
 
     fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>>
+
+    fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo>
 }
 
 @SysUISingleton
@@ -55,38 +59,45 @@
     private val packageChangeRepository: PackageChangeRepository
 ) : InstalledTilesComponentRepository {
 
-    @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>()
+    @GuardedBy("userMap") private val userMap = mutableMapOf<Int, StateFlow<List<ServiceInfo>>>()
 
     override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> =
-        synchronized(userMap) {
-            userMap.getOrPut(userId) {
-                /*
-                 * In order to query [PackageManager] for different users, this implementation will
-                 * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
-                 * context.
-                 */
-                val packageManager =
-                    if (applicationContext.userId == userId) {
-                        applicationContext.packageManager
-                    } else {
-                        applicationContext
-                            .createContextAsUser(
-                                UserHandle.of(userId),
-                                /* flags */ 0,
-                            )
-                            .packageManager
-                    }
-                packageChangeRepository
-                    .packageChanged(UserHandle.of(userId))
-                    .onStart { emit(PackageChangeModel.Empty) }
-                    .map { reloadComponents(userId, packageManager) }
-                    .distinctUntilChanged()
-                    .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1)
-            }
+        synchronized(userMap) { getForUserLocked(userId) }
+            .map { it.mapTo(mutableSetOf()) { it.componentName } }
+
+    override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> {
+        return synchronized(userMap) { getForUserLocked(userId).value }
+    }
+
+    private fun getForUserLocked(userId: Int): StateFlow<List<ServiceInfo>> {
+        return userMap.getOrPut(userId) {
+            /*
+             * In order to query [PackageManager] for different users, this implementation will
+             * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
+             * context.
+             */
+            val packageManager =
+                if (applicationContext.userId == userId) {
+                    applicationContext.packageManager
+                } else {
+                    applicationContext
+                        .createContextAsUser(
+                            UserHandle.of(userId),
+                            /* flags */ 0,
+                        )
+                        .packageManager
+                }
+            packageChangeRepository
+                .packageChanged(UserHandle.of(userId))
+                .onStart { emit(PackageChangeModel.Empty) }
+                .map { reloadComponents(userId, packageManager) }
+                .distinctUntilChanged()
+                .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), emptyList())
         }
+    }
 
     @WorkerThread
-    private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> {
+    private fun reloadComponents(userId: Int, packageManager: PackageManager): List<ServiceInfo> {
         return packageManager
             .queryIntentServicesAsUser(INTENT, FLAGS, userId)
             .mapNotNull { it.serviceInfo }
@@ -100,7 +111,6 @@
                     false
                 }
             }
-            .mapTo(mutableSetOf()) { it.componentName }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 61896f0..b7fcef4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -115,6 +115,10 @@
      * @see TileSpecRepository.setTiles
      */
     fun setTiles(specs: List<TileSpec>)
+
+    companion object {
+        val POSITION_AT_END: Int = TileSpecRepository.POSITION_AT_END
+    }
 }
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt
new file mode 100644
index 0000000..2ae3f07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.pipeline.domain.interactor
+
+import com.android.systemui.qs.pipeline.data.repository.MinimumTilesRepository
+import javax.inject.Inject
+
+class MinimumTilesInteractor
+@Inject
+constructor(
+    private val minimumTilesRepository: MinimumTilesRepository,
+) {
+    val minNumberOfTiles: Int
+        get() = minimumTilesRepository.minNumberOfTiles
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index c24113f1..56588ff 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -55,6 +55,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.graph.SignalDrawable;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityTransitionAnimator;
 import com.android.systemui.animation.Expandable;
@@ -632,12 +633,23 @@
     }
 
     public static class DrawableIcon extends Icon {
+
         protected final Drawable mDrawable;
         protected final Drawable mInvisibleDrawable;
+        private static final String TAG = "QSTileImpl";
 
         public DrawableIcon(Drawable drawable) {
             mDrawable = drawable;
-            mInvisibleDrawable = drawable.getConstantState().newDrawable();
+            Drawable.ConstantState nullableConstantState = drawable.getConstantState();
+            if (nullableConstantState == null) {
+                if (!(drawable instanceof SignalDrawable)) {
+                    Log.w(TAG, "DrawableIcon: drawable has null ConstantState"
+                            + " and is not a SignalDrawable");
+                }
+                mInvisibleDrawable = drawable;
+            } else {
+                mInvisibleDrawable = nullableConstantState.newDrawable();
+            }
         }
 
         @Override
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 f3852a2..4fd0df4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -186,7 +186,8 @@
     private val locInScreen = IntArray(2)
 
     /** Visuo-haptic long-press effects */
-    private var haveLongPressPropertiesBeenReset = true
+    var haveLongPressPropertiesBeenReset = true
+        private set
     private var paddingForLaunch = Rect()
     private var initialLongPressProperties: QSLongPressProperties? = null
     private var finalLongPressProperties: QSLongPressProperties? = null
@@ -772,7 +773,11 @@
         }
     }
 
-    override fun onActivityLaunchAnimationEnd() = resetLongPressEffectProperties()
+    override fun onActivityLaunchAnimationEnd() {
+        if (longPressEffect != null && !haveLongPressPropertiesBeenReset) {
+            resetLongPressEffectProperties()
+        }
+    }
 
     fun prepareForLaunch() {
         val startingHeight = initialLongPressProperties?.height?.toInt() ?: 0
@@ -877,8 +882,8 @@
         background.updateBounds(
             left = 0,
             top = 0,
-            right = initialLongPressProperties?.width?.toInt() ?: 0,
-            bottom = initialLongPressProperties?.height?.toInt() ?: 0,
+            right = initialLongPressProperties?.width?.toInt() ?: measuredWidth,
+            bottom = initialLongPressProperties?.height?.toInt() ?: measuredHeight,
         )
         changeCornerRadius(resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat())
         setAllColors(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index 065e89f..f0d7206 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -175,6 +175,26 @@
             )
     }
 
+    /** Log with level [LogLevel.WARNING] */
+    fun logWarning(
+        tileSpec: TileSpec,
+        message: String,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(tileSpec.getLogTag(), LogLevel.WARNING, { str1 = message }, { str1!! })
+    }
+
+    /** Log with level [LogLevel.INFO] */
+    fun logInfo(
+        tileSpec: TileSpec,
+        message: String,
+    ) {
+        tileSpec
+            .getLogBuffer()
+            .log(tileSpec.getLogTag(), LogLevel.INFO, { str1 = message }, { str1!! })
+    }
+
     fun logCustomTileUserActionDelivered(tileSpec: TileSpec) {
         tileSpec
             .getLogBuffer()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 60469c0..b057476 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.qs.tiles.dialog;
 
+import static android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING;
+
 import static com.android.settingslib.mobile.MobileMappings.getIconKey;
 import static com.android.settingslib.mobile.MobileMappings.mapIconSets;
 import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
@@ -190,7 +192,7 @@
     private DialogTransitionAnimator mDialogTransitionAnimator;
     private boolean mHasWifiEntries;
     private WifiStateWorker mWifiStateWorker;
-    private boolean mHasActiveSubId;
+    private boolean mHasActiveSubIdOnDds;
 
     @VisibleForTesting
     static final float TOAST_PARAMS_HORIZONTAL_WEIGHT = 1.0f;
@@ -298,7 +300,7 @@
                 mExecutor);
         // Listen the subscription changes
         mOnSubscriptionsChangedListener = new InternetOnSubscriptionChangedListener();
-        refreshHasActiveSubId();
+        refreshHasActiveSubIdOnDds();
         mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor,
                 mOnSubscriptionsChangedListener);
         mDefaultDataSubId = getDefaultDataSubscriptionId();
@@ -428,7 +430,7 @@
         }
         boolean isActiveOnNonDds = getActiveAutoSwitchNonDdsSubId() != SubscriptionManager
                 .INVALID_SUBSCRIPTION_ID;
-        if (!hasActiveSubId() || (!isVoiceStateInService(mDefaultDataSubId)
+        if (!hasActiveSubIdOnDds() || (!isVoiceStateInService(mDefaultDataSubId)
                 && !isDataStateInService(mDefaultDataSubId) && !isActiveOnNonDds)) {
             if (DEBUG) {
                 Log.d(TAG, "No carrier or service is out of service.");
@@ -901,23 +903,42 @@
     /**
      * @return whether there is the carrier item in the slice.
      */
-    boolean hasActiveSubId() {
+    boolean hasActiveSubIdOnDds() {
         if (isAirplaneModeEnabled() || mTelephonyManager == null) {
             return false;
         }
 
-        return mHasActiveSubId;
+        return mHasActiveSubIdOnDds;
     }
 
-    private void refreshHasActiveSubId() {
+    private static boolean isEmbeddedSubscriptionVisible(@NonNull SubscriptionInfo subInfo) {
+        if (subInfo.isEmbedded() && subInfo.getProfileClass() == PROFILE_CLASS_PROVISIONING) {
+            return false;
+        }
+        return true;
+    }
+
+    private void refreshHasActiveSubIdOnDds() {
         if (mSubscriptionManager == null) {
-            mHasActiveSubId = false;
+            mHasActiveSubIdOnDds = false;
             Log.e(TAG, "SubscriptionManager is null, set mHasActiveSubId = false");
             return;
         }
+        int dds = getDefaultDataSubscriptionId();
+        if (dds == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            mHasActiveSubIdOnDds = false;
+            Log.d(TAG, "DDS is INVALID_SUBSCRIPTION_ID");
+            return;
+        }
+        SubscriptionInfo ddsSubInfo = mSubscriptionManager.getActiveSubscriptionInfo(dds);
+        if (ddsSubInfo == null) {
+            mHasActiveSubIdOnDds = false;
+            Log.e(TAG, "Can't get DDS subscriptionInfo");
+            return;
+        }
 
-        mHasActiveSubId = mSubscriptionManager.getActiveSubscriptionIdList().length > 0;
-        Log.i(TAG, "mHasActiveSubId:" + mHasActiveSubId);
+        mHasActiveSubIdOnDds = isEmbeddedSubscriptionVisible(ddsSubInfo);
+        Log.i(TAG, "mHasActiveSubId:" + mHasActiveSubIdOnDds);
     }
 
     /**
@@ -1209,7 +1230,7 @@
 
         @Override
         public void onSubscriptionsChanged() {
-            refreshHasActiveSubId();
+            refreshHasActiveSubIdOnDds();
             updateListener();
         }
     }
@@ -1306,6 +1327,7 @@
                     Log.d(TAG, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED");
                 }
                 mConfig = MobileMappings.Config.readConfig(context);
+                refreshHasActiveSubIdOnDds();
                 updateListener();
             } else if (WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION.equals(action)) {
                 updateListener();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index 1a881b6..c9c4443 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -429,7 +429,7 @@
         }
 
         boolean isWifiEnabled = mInternetDialogController.isWifiEnabled();
-        if (!mInternetDialogController.hasActiveSubId()
+        if (!mInternetDialogController.hasActiveSubIdOnDds()
                 && (!isWifiEnabled || !isCarrierNetworkActive)) {
             mMobileNetworkLayout.setVisibility(View.GONE);
             if (mSecondaryMobileNetworkLayout != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
index d1c8030..bd2f2c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
@@ -17,15 +17,15 @@
 package com.android.systemui.qs.tiles.impl.location.domain.interactor
 
 import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
 import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
 import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel
 import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
 import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
 /** Observes location state changes providing the [LocationTileModel]. */
 class LocationTileDataInteractor
@@ -38,19 +38,7 @@
         user: UserHandle,
         triggers: Flow<DataUpdateTrigger>
     ): Flow<LocationTileModel> =
-        ConflatedCallbackFlow.conflatedCallbackFlow {
-            val initialValue = locationController.isLocationEnabled
-            trySend(LocationTileModel(initialValue))
-
-            val callback =
-                object : LocationController.LocationChangeCallback {
-                    override fun onLocationSettingsChanged(locationEnabled: Boolean) {
-                        trySend(LocationTileModel(locationEnabled))
-                    }
-                }
-            locationController.addCallback(callback)
-            awaitClose { locationController.removeCallback(callback) }
-        }
+        locationController.isLocationEnabledFlow().map { LocationTileModel(it) }
 
     override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
new file mode 100644
index 0000000..88bd224
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.content.Context
+import android.hardware.display.ColorDisplayManager
+import android.os.UserHandle
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.util.time.DateFormatUtil
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Observes screen record state changes providing the [NightDisplayTileModel]. */
+class NightDisplayTileDataInteractor
+@Inject
+constructor(
+    @Application private val context: Context,
+    private val dateFormatUtil: DateFormatUtil,
+    private val nightDisplayRepository: NightDisplayRepository,
+) : QSTileDataInteractor<NightDisplayTileModel> {
+
+    override fun tileData(
+        user: UserHandle,
+        triggers: Flow<DataUpdateTrigger>
+    ): Flow<NightDisplayTileModel> =
+        nightDisplayRepository.nightDisplayState(user).map {
+            generateModel(
+                it.autoMode,
+                it.isActivated,
+                it.startTime,
+                it.endTime,
+                it.shouldForceAutoMode,
+                it.locationEnabled
+            )
+        }
+
+    /** This checks resources and there fore does not make a binder call. */
+    override fun availability(user: UserHandle): Flow<Boolean> =
+        flowOf(ColorDisplayManager.isNightDisplayAvailable(context))
+
+    private fun generateModel(
+        autoMode: Int,
+        isNightDisplayActivated: Boolean,
+        customStartTime: LocalTime?,
+        customEndTime: LocalTime?,
+        shouldForceAutoMode: Boolean,
+        locationEnabled: Boolean,
+    ): NightDisplayTileModel {
+        if (autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) {
+            return NightDisplayTileModel.AutoModeTwilight(
+                isNightDisplayActivated,
+                shouldForceAutoMode,
+                locationEnabled,
+            )
+        } else if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME) {
+            return NightDisplayTileModel.AutoModeCustom(
+                isNightDisplayActivated,
+                shouldForceAutoMode,
+                customStartTime,
+                customEndTime,
+                dateFormatUtil.is24HourFormat,
+            )
+        } else { // auto mode off
+            return NightDisplayTileModel.AutoModeOff(isNightDisplayActivated, shouldForceAutoMode)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
new file mode 100644
index 0000000..5cee8c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.content.Intent
+import android.hardware.display.ColorDisplayManager.AUTO_MODE_CUSTOM_TIME
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles night display tile clicks. */
+class NightDisplayTileUserActionInteractor
+@Inject
+constructor(
+    private val nightDisplayRepository: NightDisplayRepository,
+    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+    private val qsLogger: QSTileLogger,
+) : QSTileUserActionInteractor<NightDisplayTileModel> {
+    override suspend fun handleInput(input: QSTileInput<NightDisplayTileModel>): Unit =
+        with(input) {
+            when (action) {
+                is QSTileUserAction.Click -> {
+                    // Enroll in forced auto mode if eligible.
+                    if (data.isEnrolledInForcedNightDisplayAutoMode) {
+                        nightDisplayRepository.setNightDisplayAutoMode(AUTO_MODE_CUSTOM_TIME, user)
+                        qsLogger.logInfo(spec, "Enrolled in forced night display auto mode")
+                    }
+                    nightDisplayRepository.setNightDisplayActivated(!data.isActivated, user)
+                }
+                is QSTileUserAction.LongClick -> {
+                    qsTileIntentUserActionHandler.handle(
+                        action.expandable,
+                        Intent(Settings.ACTION_NIGHT_DISPLAY_SETTINGS)
+                    )
+                }
+            }
+        }
+
+    companion object {
+        val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
new file mode 100644
index 0000000..6b1bd5b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.model
+
+import java.time.LocalTime
+
+/** Data model for night display tile */
+sealed interface NightDisplayTileModel {
+    val isActivated: Boolean
+    val isEnrolledInForcedNightDisplayAutoMode: Boolean
+    data class AutoModeTwilight(
+        override val isActivated: Boolean,
+        override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+        val isLocationEnabled: Boolean
+    ) : NightDisplayTileModel
+    data class AutoModeCustom(
+        override val isActivated: Boolean,
+        override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+        val startTime: LocalTime?,
+        val endTime: LocalTime?,
+        val is24HourFormat: Boolean
+    ) : NightDisplayTileModel
+    data class AutoModeOff(
+        override val isActivated: Boolean,
+        override val isEnrolledInForcedNightDisplayAutoMode: Boolean
+    ) : NightDisplayTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
new file mode 100644
index 0000000..5c2dcfc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.ui
+
+import android.content.res.Resources
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import androidx.annotation.StringRes
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import java.time.DateTimeException
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import javax.inject.Inject
+
+/** Maps [NightDisplayTileModel] to [QSTileState]. */
+class NightDisplayTileMapper
+@Inject
+constructor(
+    @Main private val resources: Resources,
+    private val theme: Resources.Theme,
+    private val logger: QSTileLogger,
+) : QSTileDataToStateMapper<NightDisplayTileModel> {
+    override fun map(config: QSTileConfig, data: NightDisplayTileModel): QSTileState =
+        QSTileState.build(resources, theme, config.uiConfig) {
+            label = resources.getString(R.string.quick_settings_night_display_label)
+            supportedActions =
+                setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+            sideViewIcon = QSTileState.SideViewIcon.None
+
+            if (data.isActivated) {
+                activationState = QSTileState.ActivationState.ACTIVE
+                val loadedIcon =
+                    Icon.Loaded(
+                        resources.getDrawable(R.drawable.qs_nightlight_icon_on, theme),
+                        contentDescription = null
+                    )
+                icon = { loadedIcon }
+            } else {
+                activationState = QSTileState.ActivationState.INACTIVE
+                val loadedIcon =
+                    Icon.Loaded(
+                        resources.getDrawable(R.drawable.qs_nightlight_icon_off, theme),
+                        contentDescription = null
+                    )
+                icon = { loadedIcon }
+            }
+
+            secondaryLabel = getSecondaryLabel(data, resources)
+
+            contentDescription =
+                if (TextUtils.isEmpty(secondaryLabel)) label
+                else TextUtils.concat(label, ", ", secondaryLabel)
+        }
+
+    private fun getSecondaryLabel(
+        data: NightDisplayTileModel,
+        resources: Resources
+    ): CharSequence? {
+        when (data) {
+            is NightDisplayTileModel.AutoModeTwilight -> {
+                if (!data.isLocationEnabled) {
+                    return null
+                } else {
+                    return resources.getString(
+                        if (data.isActivated)
+                            R.string.quick_settings_night_secondary_label_until_sunrise
+                        else R.string.quick_settings_night_secondary_label_on_at_sunset
+                    )
+                }
+            }
+            is NightDisplayTileModel.AutoModeOff -> {
+                val subtitleArray = resources.getStringArray(R.array.tile_states_night)
+                return subtitleArray[
+                    if (data.isActivated) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE]
+            }
+            is NightDisplayTileModel.AutoModeCustom -> {
+                // User-specified time, approximated to the nearest hour.
+                @StringRes val toggleTimeStringRes: Int
+                val toggleTime: LocalTime
+                if (data.isActivated) {
+                    toggleTime = data.endTime ?: return null
+                    toggleTimeStringRes = R.string.quick_settings_secondary_label_until
+                } else {
+                    toggleTime = data.startTime ?: return null
+                    toggleTimeStringRes = R.string.quick_settings_night_secondary_label_on_at
+                }
+
+                try {
+                    val formatter = if (data.is24HourFormat) formatter24Hour else formatter12Hour
+                    val formatArg = formatter.format(toggleTime)
+                    return resources.getString(toggleTimeStringRes, formatArg)
+                } catch (exception: DateTimeException) {
+                    logger.logWarning(spec, exception.message.toString())
+                    return null
+                }
+            }
+        }
+    }
+
+    private companion object {
+        val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+        val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+        val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index b88c1e5..5346b23 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -201,6 +201,7 @@
         qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
 
     override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
+
     override fun getTileLabel(): CharSequence =
         with(qsTileViewModel.config.uiConfig) {
             when (this) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
index d6325c0..a04fa38 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
 import javax.inject.Inject
 
@@ -27,4 +28,5 @@
 constructor(
     val brightnessSliderViewModel: BrightnessSliderViewModel,
     val tileGridViewModel: TileGridViewModel,
+    val editModeViewModel: EditModeViewModel,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 76bd80f..faf2bbc 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -707,7 +707,8 @@
             public void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop) {
                 if (mOverviewProxy != null) {
                     try {
-                        if (DesktopModeStatus.isEnabled() && (sysUiState.getFlags()
+                        if (DesktopModeStatus.canEnterDesktopMode(mContext)
+                                && (sysUiState.getFlags()
                                 & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) {
                             return;
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
index b91dd04..0603d21 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
@@ -24,6 +24,8 @@
  * These are the subset of transitions that can be referenced by key when asking for a scene change.
  */
 object TransitionKeys {
+    /** Reference to the gone to shade transition with split shade enabled. */
+    val GoneToSplitShade = TransitionKey("GoneToSplitShade")
 
     /** Reference to a scene transition that can collapse the shade scene instantly. */
     val CollapseShadeInstantly = TransitionKey("CollapseShadeInstantly")
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index 259a8bf..b971781 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -56,9 +56,8 @@
     }
 
     // TODO(b/298525212): remove once Compose exposes window inset bounds.
-    override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
-        val insets = super.onApplyWindowInsets(windowInsets)
-        this.windowInsets.value = insets
-        return insets
+    override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
+        this.windowInsets.value = windowInsets
+        return windowInsets
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 78704e1..c20d577 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -198,7 +198,7 @@
     private fun getDisplayWidth(context: Context): Dp {
         val point = Point()
         checkNotNull(context.display).getRealSize(point)
-        return point.x.dp
+        return point.x.toDp(context)
     }
 
     // TODO(b/298525212): remove once Compose exposes window inset bounds.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index b0af7f9..016fe57 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
 import javax.inject.Inject
@@ -70,10 +71,11 @@
                     )] = UserActionResult(Scenes.QuickSettingsShade)
             }
 
+            val downSceneKey =
+                if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+            val downTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split }
             this[Swipe(direction = SwipeDirection.Down)] =
-                UserActionResult(
-                    if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
-                )
+                UserActionResult(downSceneKey, downTransitionKey)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 494fc9b..bd90de2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -47,7 +47,6 @@
 import android.graphics.Bitmap;
 import android.graphics.Insets;
 import android.graphics.Rect;
-import android.hardware.display.DisplayManager;
 import android.net.Uri;
 import android.os.Process;
 import android.os.UserHandle;
@@ -208,8 +207,7 @@
     @Nullable
     private final ScreenshotSoundController mScreenshotSoundController;
     private final PhoneWindow mWindow;
-    private final DisplayManager mDisplayManager;
-    private final int mDisplayId;
+    private final Display mDisplay;
     private final ScrollCaptureExecutor mScrollCaptureExecutor;
     private final ScreenshotNotificationSmartActionsProvider
             mScreenshotNotificationSmartActionsProvider;
@@ -249,7 +247,6 @@
     @AssistedInject
     ScreenshotController(
             Context context,
-            DisplayManager displayManager,
             WindowManager windowManager,
             FeatureFlags flags,
             ScreenshotViewProxy.Factory viewProxyFactory,
@@ -271,12 +268,13 @@
             AssistContentRequester assistContentRequester,
             MessageContainerController messageContainerController,
             Provider<ScreenshotSoundController> screenshotSoundController,
-            @Assisted int displayId,
+            @Assisted Display display,
             @Assisted boolean showUIOnExternalDisplay
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
         mActionsProviderFactory = actionsProviderFactory;
-        mNotificationsController = screenshotNotificationsControllerFactory.create(displayId);
+        mNotificationsController = screenshotNotificationsControllerFactory.create(
+                display.getDisplayId());
         mUiEventLogger = uiEventLogger;
         mImageExporter = imageExporter;
         mImageCapture = imageCapture;
@@ -290,11 +288,9 @@
         mScreenshotHandler = timeoutHandler;
         mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
 
-
-        mDisplayId = displayId;
-        mDisplayManager = displayManager;
+        mDisplay = display;
         mWindowManager = windowManager;
-        final Context displayContext = context.createDisplayContext(getDisplay());
+        final Context displayContext = context.createDisplayContext(display);
         mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
         mFlags = flags;
         mActionIntentExecutor = actionIntentExecutor;
@@ -302,7 +298,7 @@
         mMessageContainerController = messageContainerController;
         mAssistContentRequester = assistContentRequester;
 
-        mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId);
+        mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId());
 
         mScreenshotHandler.setOnTimeoutRunnable(() -> {
             if (DEBUG_UI) {
@@ -328,7 +324,7 @@
                 });
 
         // Sound is only reproduced from the controller of the default display.
-        if (displayId == Display.DEFAULT_DISPLAY) {
+        if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) {
             mScreenshotSoundController = screenshotSoundController.get();
         } else {
             mScreenshotSoundController = null;
@@ -356,7 +352,7 @@
         if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN
                 && screenshot.getBitmap() == null) {
             Rect bounds = getFullScreenRect();
-            screenshot.setBitmap(mImageCapture.captureDisplay(mDisplayId, bounds));
+            screenshot.setBitmap(mImageCapture.captureDisplay(mDisplay.getDisplayId(), bounds));
             screenshot.setScreenBounds(bounds);
         }
 
@@ -459,7 +455,7 @@
     }
 
     private boolean shouldShowUi() {
-        return mDisplayId == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay;
+        return mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay;
     }
 
     void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
@@ -618,7 +614,7 @@
 
     private void requestScrollCapture(UserHandle owner) {
         mScrollCaptureExecutor.requestScrollCapture(
-                mDisplayId,
+                mDisplay.getDisplayId(),
                 mWindow.getDecorView().getWindowToken(),
                 (response) -> {
                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
@@ -641,7 +637,8 @@
         }
         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0,
                 response.getPackageName());
-        Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, getFullScreenRect());
+        Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplay.getDisplayId(),
+                getFullScreenRect());
         if (newScreenshot == null) {
             Log.e(TAG, "Failed to capture current screenshot for scroll transition!");
             return;
@@ -819,7 +816,8 @@
     private void saveScreenshotInBackground(
             ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) {
         ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
-                requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), mDisplayId);
+                requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(),
+                mDisplay.getDisplayId());
         future.addListener(() -> {
             try {
                 ImageExporter.Result result = future.get();
@@ -861,7 +859,7 @@
         data.mActionsReadyListener = actionsReadyListener;
         data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
         data.owner = owner;
-        data.displayId = mDisplayId;
+        data.displayId = mDisplay.getDisplayId();
 
         if (mSaveInBgTask != null) {
             // just log success/failure for the pre-existing screenshot
@@ -986,13 +984,9 @@
         }
     }
 
-    private Display getDisplay() {
-        return mDisplayManager.getDisplay(mDisplayId);
-    }
-
     private Rect getFullScreenRect() {
         DisplayMetrics displayMetrics = new DisplayMetrics();
-        getDisplay().getRealMetrics(displayMetrics);
+        mDisplay.getRealMetrics(displayMetrics);
         return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels);
     }
 
@@ -1026,12 +1020,12 @@
     @AssistedFactory
     public interface Factory {
         /**
-         * Creates an instance of the controller for that specific displayId.
+         * Creates an instance of the controller for that specific display.
          *
-         * @param displayId:               display to capture
-         * @param showUIOnExternalDisplay: Whether the UI should be shown if this is an external
-         *                                 display.
+         * @param display                 display to capture
+         * @param showUIOnExternalDisplay Whether the UI should be shown if this is an external
+         *                                display.
          */
-        ScreenshotController create(int displayId, boolean showUIOnExternalDisplay);
+        ScreenshotController create(Display display, boolean showUIOnExternalDisplay);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index e56a4f4..40d709d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -68,11 +68,13 @@
         onSaved: (Uri?) -> Unit,
         requestCallback: RequestCallback
     ) {
-        val displayIds = getDisplaysToScreenshot(screenshotRequest.type)
+        val displays = getDisplaysToScreenshot(screenshotRequest.type)
         val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
-        displayIds.forEach { displayId: Int ->
+        displays.forEach { display ->
+            val displayId = display.displayId
             Log.d(TAG, "Executing screenshot for display $displayId")
             dispatchToController(
+                display = display,
                 rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
                 onSaved =
                     if (displayId == Display.DEFAULT_DISPLAY) {
@@ -85,6 +87,7 @@
 
     /** All logging should be triggered only by this method. */
     private suspend fun dispatchToController(
+        display: Display,
         rawScreenshotData: ScreenshotData,
         onSaved: (Uri?) -> Unit,
         callback: RequestCallback
@@ -104,8 +107,7 @@
         logScreenshotRequested(screenshotData)
         Log.d(TAG, "Screenshot request: $screenshotData")
         try {
-            getScreenshotController(screenshotData.displayId)
-                .handleScreenshot(screenshotData, onSaved, callback)
+            getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback)
         } catch (e: IllegalStateException) {
             Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e)
             onFailedScreenshotRequest(screenshotData, callback)
@@ -135,12 +137,13 @@
         callback.reportError()
     }
 
-    private suspend fun getDisplaysToScreenshot(requestType: Int): List<Int> {
+    private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> {
+        val allDisplays = displays.first()
         return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
             // If this is a provided image, let's show the UI on the default display only.
-            listOf(Display.DEFAULT_DISPLAY)
+            allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY }
         } else {
-            displays.first().filter { it.type in ALLOWED_DISPLAY_TYPES }.map { it.displayId }
+            allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES }
         }
     }
 
@@ -170,9 +173,9 @@
         screenshotControllers.clear()
     }
 
-    private fun getScreenshotController(id: Int): ScreenshotController {
-        return screenshotControllers.computeIfAbsent(id) {
-            screenshotControllerFactory.create(id, /* showUIOnExternalDisplay= */ false)
+    private fun getScreenshotController(display: Display): ScreenshotController {
+        return screenshotControllers.computeIfAbsent(display.displayId) {
+            screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 281857f..6367d44b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -51,9 +51,9 @@
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.android.systemui.util.kotlin.collectFlow
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -145,7 +145,7 @@
 
     /** Returns a flow that tracks whether communal hub is available. */
     fun communalAvailable(): Flow<Boolean> =
-        or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+        anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
 
     /**
      * Creates the container view containing the glanceable hub UI.
@@ -248,7 +248,7 @@
         // transition to the bouncer would be incorrectly intercepted by the hub.
         collectFlow(
             containerView,
-            or(
+            anyOf(
                 keyguardInteractor.primaryBouncerShowing,
                 keyguardInteractor.alternateBouncerShowing
             ),
@@ -267,7 +267,7 @@
         )
         collectFlow(
             containerView,
-            and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
+            allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
             {
                 shadeShowing = it
                 updateTouchHandlingState()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index ac76bec..d15a488 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.qs.ui.adapter.QSSceneAdapter
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
 import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -152,11 +153,13 @@
                 else -> Scenes.Lockscreen
             }
 
+        val upTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split }
+
         val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
 
         return buildMap {
             if (!isCustomizing) {
-                this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+                this[Swipe(SwipeDirection.Up)] = UserActionResult(up, upTransitionKey)
             } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
             down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 7983db1..2446473 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -199,12 +199,14 @@
     protected boolean mPowerPluggedInWired;
     protected boolean mPowerPluggedInWireless;
     protected boolean mPowerPluggedInDock;
+    protected int mChargingSpeed;
 
     private boolean mPowerCharged;
+    /** Whether the battery defender is triggered. */
     private boolean mBatteryDefender;
+    /** Whether the battery defender is triggered with the device plugged. */
     private boolean mEnableBatteryDefender;
     private boolean mIncompatibleCharger;
-    protected int mChargingSpeed;
     private int mChargingWattage;
     private int mBatteryLevel;
     private boolean mBatteryPresent = true;
@@ -1244,7 +1246,7 @@
             mChargingSpeed = status.getChargingSpeed(mContext);
             mBatteryLevel = status.level;
             mBatteryPresent = status.present;
-            mBatteryDefender = status.isBatteryDefender();
+            mBatteryDefender = isBatteryDefender(status);
             // when the battery is overheated, device doesn't charge so only guard on pluggedIn:
             mEnableBatteryDefender = mBatteryDefender && status.isPluggedIn();
             mIncompatibleCharger = status.incompatibleCharger.orElse(false);
@@ -1516,6 +1518,11 @@
         return mPowerPluggedIn;
     }
 
+    /** Return true if the device is under the battery defender mode. */
+    protected boolean isBatteryDefender(BatteryStatus status) {
+        return status.isBatteryDefender();
+    }
+
     private boolean isCurrentUser(int userId) {
         return getCurrentUser() == userId;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 222b070..14e14f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -14,7 +14,6 @@
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.Dumpable
 import com.android.systemui.ExpandHelper
-import com.android.systemui.Flags.nsslFalsingFix
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy
 import com.android.systemui.classifier.Classifier
@@ -884,9 +883,7 @@
                     isDraggingDown = false
                     isTrackpadReverseScroll = false
                     shadeRepository.setLegacyLockscreenShadeTracking(false)
-                    if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) {
-                        return true
-                    }
+                    return true
                 } else {
                     stopDragging()
                     return false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 70632d5..79218ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
 
 import android.animation.Animator;
@@ -49,6 +50,7 @@
 import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
 import com.android.systemui.keyguard.MigrateClocksToBlueprint;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.res.R;
 import com.android.systemui.scene.domain.interactor.SceneInteractor;
@@ -108,6 +110,7 @@
     private final UiEventLogger mUiEventLogger;
     private final Lazy<InteractionJankMonitor> mInteractionJankMonitorLazy;
     private final JavaAdapter mJavaAdapter;
+    private final Lazy<KeyguardTransitionInteractor> mKeyguardTransitionInteractorLazy;
     private final Lazy<ShadeInteractor> mShadeInteractorLazy;
     private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
     private final Lazy<SceneInteractor> mSceneInteractorLazy;
@@ -175,6 +178,7 @@
             UiEventLogger uiEventLogger,
             Lazy<InteractionJankMonitor> interactionJankMonitorLazy,
             JavaAdapter javaAdapter,
+            Lazy<KeyguardTransitionInteractor> keyguardTransitionInteractor,
             Lazy<ShadeInteractor> shadeInteractorLazy,
             Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
             Lazy<SceneInteractor> sceneInteractorLazy,
@@ -182,6 +186,7 @@
         mUiEventLogger = uiEventLogger;
         mInteractionJankMonitorLazy = interactionJankMonitorLazy;
         mJavaAdapter = javaAdapter;
+        mKeyguardTransitionInteractorLazy = keyguardTransitionInteractor;
         mShadeInteractorLazy = shadeInteractorLazy;
         mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
         mSceneInteractorLazy = sceneInteractorLazy;
@@ -193,6 +198,14 @@
 
     @Override
     public void start() {
+        mJavaAdapter.alwaysCollectFlow(
+                mKeyguardTransitionInteractorLazy.get().isFinishedInState(GONE),
+                (Boolean isFinishedInState) -> {
+                    if (isFinishedInState) {
+                        setLeaveOpenOnKeyguardHide(false);
+                    }
+                });
+
         mJavaAdapter.alwaysCollectFlow(mShadeInteractorLazy.get().isAnyExpanded(),
                 this::onShadeOrQsExpanded);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index c17da4b..0524589 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -32,6 +32,7 @@
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -56,10 +57,10 @@
 import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
 import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
-import com.android.systemui.statusbar.phone.ui.StatusBarIconList;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconControllerImpl;
+import com.android.systemui.statusbar.phone.ui.StatusBarIconList;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import dagger.Binds;
@@ -209,14 +210,16 @@
     /** */
     @Provides
     @SysUISingleton
-    static ActivityTransitionAnimator provideActivityTransitionAnimator() {
-        return new ActivityTransitionAnimator();
+    static ActivityTransitionAnimator provideActivityTransitionAnimator(
+            @Main Executor mainExecutor) {
+        return new ActivityTransitionAnimator(mainExecutor);
     }
 
     /** */
     @Provides
     @SysUISingleton
-    static DialogTransitionAnimator provideDialogTransitionAnimator(IDreamManager dreamManager,
+    static DialogTransitionAnimator provideDialogTransitionAnimator(@Main Executor mainExecutor,
+            IDreamManager dreamManager,
             KeyguardStateController keyguardStateController,
             Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
             InteractionJankMonitor interactionJankMonitor,
@@ -243,7 +246,7 @@
             }
         };
         return new DialogTransitionAnimator(
-                callback, interactionJankMonitor, animationFeatureFlags);
+                mainExecutor, callback, interactionJankMonitor, animationFeatureFlags);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
index 0c341cc..ec3c7d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
@@ -27,6 +27,9 @@
  * (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
  */
 public interface NotificationActivityStarter {
+    /** Called when the user clicks on the notification bubble icon. */
+    void onNotificationBubbleIconClicked(NotificationEntry entry);
+
     /** Called when the user clicks on the surface of a notification. */
     void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index d10fac6..6487d55 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -117,11 +117,14 @@
         Notification notification = sbn.getNotification();
         if (notification.contentIntent != null || notification.fullScreenIntent != null
                 || row.getEntry().isBubble()) {
+            row.setBubbleClickListener(v ->
+                    mNotificationActivityStarter.onNotificationBubbleIconClicked(row.getEntry()));
             row.setOnClickListener(this);
             row.setOnDragSuccessListener(mOnDragSuccessListener);
         } else {
             row.setOnClickListener(null);
             row.setOnDragSuccessListener(null);
+            row.setBubbleClickListener(null);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 61cdea1..d2d0aaa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.row;
 
 import static com.android.systemui.Flags.notificationBackgroundTintOptimization;
+import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.BOTTOM;
+import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.TOP;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -38,10 +40,12 @@
 import com.android.settingslib.Utils;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.res.R;
+import com.android.systemui.shade.TouchLogger;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.notification.FakeShadowView;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.SourceType;
+import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
@@ -353,12 +357,13 @@
     @Override
     public long performRemoveAnimation(long duration, long delay, float translationDirection,
             boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable,
-            AnimatorListenerAdapter animationListener) {
+            AnimatorListenerAdapter animationListener, ClipSide clipSide) {
         enableAppearDrawing(true);
         mIsHeadsUpAnimation = isHeadsUpAnimation;
         if (mDrawingAppearAnimation) {
             startAppearAnimation(false /* isAppearing */, translationDirection,
-                    delay, duration, onStartedRunnable, onFinishedRunnable, animationListener);
+                    delay, duration, onStartedRunnable, onFinishedRunnable, animationListener,
+                    clipSide);
         } else {
             if (onStartedRunnable != null) {
                 onStartedRunnable.run();
@@ -377,13 +382,13 @@
         mIsHeadsUpAnimation = isHeadsUpAppear;
         if (mDrawingAppearAnimation) {
             startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay,
-                    duration, null, null, null);
+                    duration, null, null, null, ClipSide.BOTTOM);
         }
     }
 
     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
             long duration, final Runnable onStartedRunnable, final Runnable onFinishedRunnable,
-            AnimatorListenerAdapter animationListener) {
+            AnimatorListenerAdapter animationListener, ClipSide clipSide) {
         mAnimationTranslationY = translationDirection * getActualHeight();
         cancelAppearAnimation();
         if (mAppearAnimationFraction == -1.0f) {
@@ -405,9 +410,16 @@
             mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE;
             targetValue = 0.0f;
         }
+
+        if (NotificationHeadsUpCycling.isEnabled()) {
+            // TODO(b/316404716): add avalanche filtering
+            mCurrentAppearInterpolator = Interpolators.LINEAR;
+        }
+
         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
                 targetValue);
-        if (NotificationsImprovedHunAnimation.isEnabled()) {
+        if (NotificationsImprovedHunAnimation.isEnabled()
+                || NotificationHeadsUpCycling.isEnabled()) {
             mAppearAnimator.setInterpolator(mCurrentAppearInterpolator);
         } else {
             mAppearAnimator.setInterpolator(Interpolators.LINEAR);
@@ -417,7 +429,12 @@
         mAppearAnimator.addUpdateListener(animation -> {
             mAppearAnimationFraction = (float) animation.getAnimatedValue();
             updateAppearAnimationAlpha();
-            updateAppearRect();
+            if (NotificationHeadsUpCycling.isEnabled()) {
+                // For cycling out, we want the HUN to be clipped from the top.
+                updateAppearRect(clipSide);
+            } else {
+                updateAppearRect();
+            }
             invalidate();
         });
         if (animationListener != null) {
@@ -425,7 +442,11 @@
         }
         // we need to apply the initial state already to avoid drawn frames in the wrong state
         updateAppearAnimationAlpha();
-        updateAppearRect();
+        if (NotificationHeadsUpCycling.isEnabled()) {
+            updateAppearRect(clipSide);
+        } else {
+            updateAppearRect();
+        }
         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
             private boolean mRunWithoutInterruptions;
 
@@ -507,14 +528,18 @@
         enableAppearDrawing(false);
     }
 
-    private void updateAppearRect() {
+    /**
+     * Update the View's Rect clipping to fit the appear animation
+     * @param clipSide Which side if view we want to clip from
+     */
+    private void updateAppearRect(ClipSide clipSide) {
         float interpolatedFraction =
-                NotificationsImprovedHunAnimation.isEnabled() ? mAppearAnimationFraction
+                NotificationsImprovedHunAnimation.isEnabled()
+                        || NotificationHeadsUpCycling.isEnabled() ? mAppearAnimationFraction
                         : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction);
         mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY;
-        final int actualHeight = getActualHeight();
-        float bottom = actualHeight * interpolatedFraction;
-
+        final int fullHeight = getActualHeight();
+        float height = fullHeight * interpolatedFraction;
         if (mTargetPoint != null) {
             int width = getWidth();
             float fraction = 1 - mAppearAnimationFraction;
@@ -523,13 +548,26 @@
                     mAnimationTranslationY
                             + (mAnimationTranslationY - mTargetPoint.y) * fraction,
                     width - (width - mTargetPoint.x) * fraction,
-                    actualHeight - (actualHeight - mTargetPoint.y) * fraction);
+                    fullHeight - (fullHeight - mTargetPoint.y) * fraction);
         } else {
-            setOutlineRect(0, mAppearAnimationTranslation, getWidth(),
-                    bottom + mAppearAnimationTranslation);
+            if (clipSide == TOP) {
+                setOutlineRect(
+                        0,
+                        /* top= */ fullHeight - height,
+                        getWidth(),
+                        /* bottom= */ fullHeight
+                );
+            } else if (clipSide == BOTTOM) {
+                setOutlineRect(0, mAppearAnimationTranslation, getWidth(),
+                        height + mAppearAnimationTranslation);
+            }
         }
     }
 
+    private void updateAppearRect() {
+        updateAppearRect(ClipSide.BOTTOM);
+    }
+
     private float getInterpolatedAppearAnimationFraction() {
 
         if (mAppearAnimationFraction >= 0) {
@@ -539,11 +577,36 @@
     }
 
     private void updateAppearAnimationAlpha() {
-        float contentAlphaProgress = MathUtils.constrain(mAppearAnimationFraction,
-                ALPHA_APPEAR_START_FRACTION, ALPHA_APPEAR_END_FRACTION);
-        float range = ALPHA_APPEAR_END_FRACTION - ALPHA_APPEAR_START_FRACTION;
-        float alpha = (contentAlphaProgress - ALPHA_APPEAR_START_FRACTION) / range;
-        setContentAlpha(Interpolators.ALPHA_IN.getInterpolation(alpha));
+        updateAppearAnimationContentAlpha(
+                mAppearAnimationFraction,
+                ALPHA_APPEAR_START_FRACTION,
+                ALPHA_APPEAR_END_FRACTION,
+                Interpolators.ALPHA_IN
+        );
+    }
+
+    /**
+     * Update the alpha value of the content view during the appear animation. We suppose that the
+     * content alpha changes from 0 to 1 during some part of the appear animation.
+     * @param appearFraction the current appearFraction, should be in the range of [0, 1], where
+     *                       1 represents fully appeared
+     * @param startFraction the appear fraction when the content view should be
+     *      *                    fully transparent
+     * @param endFraction the appear fraction when the content view should be
+     *                    fully in-transparent, should be greater or equals to startFraction
+     * @param interpolator the interpolator to update the alpha
+     */
+    private void updateAppearAnimationContentAlpha(
+            float appearFraction,
+            float startFraction,
+            float endFraction,
+            Interpolator interpolator
+    ) {
+        float contentAlphaProgress = MathUtils.constrain(appearFraction, startFraction,
+                endFraction);
+        float range = endFraction - startFraction;
+        float alpha = (contentAlphaProgress - startFraction) / range;
+        setContentAlpha(interpolator.getInterpolation(alpha));
     }
 
     private void setContentAlpha(float contentAlpha) {
@@ -745,6 +808,12 @@
         }
     }
 
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return TouchLogger.logDispatchTouch(
+                getClass().getSimpleName(), ev, super.dispatchTouchEvent(ev));
+    }
+
     /**
      * SourceType which should be reset when this View is detached
      * @param sourceType will be reset on View detached
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 5e3df7b..747cb3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -375,6 +375,8 @@
             };
 
     private OnClickListener mOnClickListener;
+    @Nullable
+    private OnClickListener mBubbleClickListener;
     private OnDragSuccessListener mOnDragSuccessListener;
     private boolean mHeadsupDisappearRunning;
     private View mChildAfterViewWhenDismissed;
@@ -1234,14 +1236,19 @@
     /**
      * The click listener for the bubble button.
      */
+    @Nullable
     public View.OnClickListener getBubbleClickListener() {
-        return v -> {
-            if (mBubblesManagerOptional.isPresent()) {
-                mBubblesManagerOptional.get()
-                        .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
-            }
-            mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
-        };
+        return mBubbleClickListener;
+    }
+
+    /**
+     * Sets the click listener for the bubble button.
+     */
+    public void setBubbleClickListener(@Nullable OnClickListener l) {
+        mBubbleClickListener = l;
+        // ensure listener is passed to the content views
+        mPrivateLayout.updateBubbleButton(mEntry);
+        mPublicLayout.updateBubbleButton(mEntry);
     }
 
     /**
@@ -3069,7 +3076,7 @@
             boolean isHeadsUpAnimation,
             Runnable onStartedRunnable,
             Runnable onFinishedRunnable,
-            AnimatorListenerAdapter animationListener) {
+            AnimatorListenerAdapter animationListener, ClipSide clipSide) {
         if (mMenuRow != null && mMenuRow.isMenuVisible()) {
             Animator anim = getTranslateViewAnimator(0f, null /* listener */);
             if (anim != null) {
@@ -3085,7 +3092,7 @@
                     public void onAnimationEnd(Animator animation) {
                         ExpandableNotificationRow.super.performRemoveAnimation(
                                 duration, delay, translationDirection, isHeadsUpAnimation,
-                                null, onFinishedRunnable, animationListener);
+                                null, onFinishedRunnable, animationListener, ClipSide.BOTTOM);
                     }
                 });
                 anim.start();
@@ -3093,7 +3100,8 @@
             }
         }
         return super.performRemoveAnimation(duration, delay, translationDirection,
-                isHeadsUpAnimation, onStartedRunnable, onFinishedRunnable, animationListener);
+                isHeadsUpAnimation, onStartedRunnable, onFinishedRunnable, animationListener,
+                clipSide);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 05e8717..2af119f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -362,17 +362,17 @@
 
     /**
      * Perform a remove animation on this view.
-     * @param duration The duration of the remove animation.
-     * @param delay The delay of the animation
+     *
+     * @param duration             The duration of the remove animation.
+     * @param delay                The delay of the animation
      * @param translationDirection The direction value from [-1 ... 1] indicating in which the
      *                             animation should be performed. A value of -1 means that The
      *                             remove animation should be performed upwards,
      *                             such that the  child appears to be going away to the top. 1
      *                             Should mean the opposite.
-     * @param isHeadsUpAnimation Is this a headsUp animation.
-     * @param onFinishedRunnable A runnable which should be run when the animation is finished.
-     * @param animationListener An animation listener to add to the animation.
-     *
+     * @param isHeadsUpAnimation   Is this a headsUp animation.
+     * @param onFinishedRunnable   A runnable which should be run when the animation is finished.
+     * @param animationListener    An animation listener to add to the animation.
      * @return The additional delay, in milliseconds, that this view needs to add before the
      * animation starts.
      */
@@ -380,7 +380,12 @@
             long delay, float translationDirection, boolean isHeadsUpAnimation,
             Runnable onStartedRunnable,
             Runnable onFinishedRunnable,
-            AnimatorListenerAdapter animationListener);
+            AnimatorListenerAdapter animationListener, ClipSide clipSide);
+
+    public enum ClipSide {
+        TOP,
+        BOTTOM
+    }
 
     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
         performAddAnimation(delay, duration, isHeadsUpAppear, null);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
index 816e5c1..db3cf5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.notification.row
 
 import android.app.Flags
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
 import javax.inject.Inject
 
 /**
@@ -27,11 +28,14 @@
     fun shouldApplyCompactStyle(): Boolean
 }
 
-class HeadsUpStyleProviderImpl @Inject constructor() : HeadsUpStyleProvider {
+class HeadsUpStyleProviderImpl
+@Inject
+constructor(private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore) :
+    HeadsUpStyleProvider {
 
-    /**
-     * TODO(b/270709257) This feature is under development. This method returns Compact when the
-     *   flag is enabled for fish fooding purpose.
-     */
-    override fun shouldApplyCompactStyle(): Boolean = Flags.compactHeadsUpNotification()
+    override fun shouldApplyCompactStyle(): Boolean {
+        // Use compact HUN for immersive mode.
+        return Flags.compactHeadsUpNotification() &&
+            statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 162e8af..291dc13 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -252,7 +252,7 @@
             float translationDirection, boolean isHeadsUpAnimation,
             Runnable onStartedRunnable,
             Runnable onFinishedRunnable,
-            AnimatorListenerAdapter animationListener) {
+            AnimatorListenerAdapter animationListener, ClipSide clipSide) {
         // TODO: Use duration
         if (onStartedRunnable != null) {
             onStartedRunnable.run();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt
index 9a54de1..2527af8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewbinder
 
+import android.util.Log
 import android.view.MotionEvent
 import android.view.View
 import android.view.View.OnTouchListener
@@ -72,7 +73,6 @@
     var isTouchEnabled = false
 
     override fun onTouch(v: View, ev: MotionEvent): Boolean {
-        val result = false
         if (ev.action == MotionEvent.ACTION_UP) {
             view.setLastActionUpTime(ev.eventTime)
         }
@@ -82,13 +82,22 @@
         }
         if (ev.action == MotionEvent.ACTION_UP) {
             // If this is a false tap, capture the even so it doesn't result in a click.
-            return falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)
+            return falsingManager.isFalseTap(FalsingManager.LOW_PENALTY).also {
+                if (it) {
+                    Log.d(v::class.simpleName ?: TAG, "capturing false tap")
+                }
+            }
         }
-        return result
+        return false
     }
 
     override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false
 
     /** Use [onTouch] instead. */
     override fun onTouchEvent(ev: MotionEvent): Boolean = false
+
+    companion object {
+        private const val TAG = "ActivatableNotificationViewBinder"
+    }
 }
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt
index 0344b32..d4f8ea3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt
@@ -33,7 +33,12 @@
     /** Is the heads-up cycling animation enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationContentAlphaOptimization()
+        get() = Flags.notificationHeadsUpCycling()
+
+    /** Whether to animate the bottom line when transiting from a tall HUN to a short HUN */
+    @JvmStatic
+    inline val animateTallToShort
+        get() = false
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index e520957..5f4e832 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -293,6 +293,8 @@
     }
 
     String getAvalancheShowingHunKey() {
+        // If we don't have a previous showing hun, we don't consider the showing hun as avalanche
+        if (isNullAvalancheKey(getAvalanchePreviousHunKey())) return "";
         return mAvalancheController.getShowingHunKey();
     }
 
@@ -300,6 +302,11 @@
         return mAvalancheController.getPreviousHunKey();
     }
 
+    boolean isNullAvalancheKey(String key) {
+        if (key == null || key.isEmpty()) return true;
+        return key.equals("HeadsUpEntry null") || key.equals("HeadsUpEntry.mEntry null");
+    }
+
     void setOverExpansion(float overExpansion) {
         mOverExpansion = overExpansion;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
index 5551ab4..bd7bd59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
@@ -70,13 +70,14 @@
     }
 
     override fun performRemoveAnimation(
-        duration: Long,
-        delay: Long,
-        translationDirection: Float,
-        isHeadsUpAnimation: Boolean,
-        onStartedRunnable: Runnable?,
-        onFinishedRunnable: Runnable?,
-        animationListener: AnimatorListenerAdapter?
+            duration: Long,
+            delay: Long,
+            translationDirection: Float,
+            isHeadsUpAnimation: Boolean,
+            onStartedRunnable: Runnable?,
+            onFinishedRunnable: Runnable?,
+            animationListener: AnimatorListenerAdapter?,
+            clipSide: ClipSide
     ): Long {
         return 0
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 773a6bf..bfc7425 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -112,6 +112,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
+import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor;
 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
@@ -151,7 +152,6 @@
 public class NotificationStackScrollLayout
         extends ViewGroup
         implements Dumpable, NotificationScrollView {
-
     public static final float BACKGROUND_ALPHA_DIMMED = 0.7f;
     private static final String TAG = "StackScroller";
     private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE);
@@ -1469,9 +1469,10 @@
     public void setExpandedHeight(float height) {
         final boolean skipHeightUpdate = shouldSkipHeightUpdate();
 
-        // when scene framework is enabled, updateStackPosition is already called by
-        // updateTopPadding every time the stack moves, so skip it here to avoid flickering.
-        if (!SceneContainerFlag.isEnabled()) {
+        // when scene framework is enabled and in single shade, updateStackPosition is already
+        // called by updateTopPadding every time the stack moves, so skip it here to avoid
+        // flickering.
+        if (!SceneContainerFlag.isEnabled() || mShouldUseSplitNotificationShade) {
             updateStackPosition();
         }
 
@@ -3143,6 +3144,11 @@
                 type = row.wasJustClicked()
                         ? AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
                         : AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
+                if (NotificationHeadsUpCycling.isEnabled()) {
+                    if (mStackScrollAlgorithm.isCyclingOut(row, mAmbientState)) {
+                        type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT;
+                    }
+                }
                 if (row.isChildInGroup()) {
                     // We can otherwise get stuck in there if it was just isolated
                     row.setHeadsUpAnimatingAway(false);
@@ -3163,6 +3169,11 @@
                     if (pinnedAndClosed || shouldHunAppearFromTheBottom) {
                         // Our custom add animation
                         type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
+                        if (NotificationHeadsUpCycling.isEnabled()) {
+                            if (mStackScrollAlgorithm.isCyclingIn(row, mAmbientState)) {
+                                type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN;
+                            }
+                        }
                     } else {
                         // Normal add animation
                         type = AnimationEvent.ANIMATION_TYPE_ADD;
@@ -6134,6 +6145,22 @@
                         .animateTopInset()
                         .animateY()
                         .animateZ(),
+
+                // ANIMATION_TYPE_HEADS_UP_CYCLING_OUT
+                new AnimationFilter()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ()
+                        .hasDelays(),
+
+                // ANIMATION_TYPE_HEADS_UP_CYCLING_IN
+                new AnimationFilter()
+                        .animateHeight()
+                        .animateTopInset()
+                        .animateY()
+                        .animateZ()
+                        .hasDelays(),
         };
 
         static int[] LENGTHS = new int[]{
@@ -6185,6 +6212,12 @@
 
                 // ANIMATION_TYPE_EVERYTHING
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
+
+                // ANIMATION_TYPE_HEADS_UP_CYCLING_OUT
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_CYCLING,
+
+                // ANIMATION_TYPE_HEADS_UP_CYCLING_IN
+                StackStateAnimator.ANIMATION_DURATION_HEADS_UP_CYCLING,
         };
 
         static final int ANIMATION_TYPE_ADD = 0;
@@ -6203,6 +6236,8 @@
         static final int ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK = 13;
         static final int ANIMATION_TYPE_HEADS_UP_OTHER = 14;
         static final int ANIMATION_TYPE_EVERYTHING = 15;
+        static final int ANIMATION_TYPE_HEADS_UP_CYCLING_OUT = 16;
+        static final int ANIMATION_TYPE_HEADS_UP_CYCLING_IN = 17;
 
         final long eventStartTime;
         final ExpandableView mChangingView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 3011bc2..c1c63cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -23,7 +23,6 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.server.notification.Flags.screenshareNotificationHiding;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
-import static com.android.systemui.Flags.nsslFalsingFix;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener;
@@ -2062,32 +2061,18 @@
             }
             boolean horizontalSwipeWantsIt = false;
             boolean scrollerWantsIt = false;
-            if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) {
-                // Reverse the order relative to the else statement. onScrollTouch will reset on an
-                // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
-                if (mLongPressedView == null && !mView.isBeingDragged()
-                        && !expandingNotification
-                        && !mView.getExpandedInThisMotion()
-                        && !onlyScrollingInThisMotion
-                        && !mView.getDisallowDismissInThisMotion()) {
-                    horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
-                }
-                if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
-                        && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
-                    scrollerWantsIt = mView.onScrollTouch(ev);
-                }
-            } else {
-                if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
-                        && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
-                    scrollerWantsIt = mView.onScrollTouch(ev);
-                }
-                if (mLongPressedView == null && !mView.isBeingDragged()
-                        && !expandingNotification
-                        && !mView.getExpandedInThisMotion()
-                        && !onlyScrollingInThisMotion
-                        && !mView.getDisallowDismissInThisMotion()) {
-                    horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
-                }
+            // NOTE: the order of these is important. If reversed, onScrollTouch will reset on an
+            // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes.
+            if (mLongPressedView == null && !mView.isBeingDragged()
+                    && !expandingNotification
+                    && !mView.getExpandedInThisMotion()
+                    && !onlyScrollingInThisMotion
+                    && !mView.getDisallowDismissInThisMotion()) {
+                horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
+            }
+            if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping()
+                    && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) {
+                scrollerWantsIt = mView.onScrollTouch(ev);
             }
 
             // Check if we need to clear any snooze leavebehinds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index d0cebae..0fcfc4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -38,6 +38,7 @@
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling;
 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
 
 import java.util.ArrayList;
@@ -75,6 +76,7 @@
     private float mSmallCornerRadius;
     private float mLargeCornerRadius;
     private int mHeadsUpAppearHeightBottom;
+    private int mHeadsUpCyclingPadding;
 
     public StackScrollAlgorithm(
             Context context,
@@ -99,6 +101,8 @@
                 R.dimen.heads_up_status_bar_padding);
         mHeadsUpAppearStartAboveScreen = res.getDimensionPixelSize(
                 R.dimen.heads_up_appear_y_above_screen);
+        mHeadsUpCyclingPadding = context.getResources()
+                .getDimensionPixelSize(R.dimen.heads_up_cycling_padding);
         mPinnedZTranslationExtra = res.getDimensionPixelSize(
                 R.dimen.heads_up_pinned_elevation);
         mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
@@ -348,7 +352,8 @@
                     && !firstHeadsUp
                     && (isHeadsUp || child.isHeadsUpAnimatingAway())
                     && newNotificationEnd > firstHeadsUpEnd
-                    && !ambientState.isShadeExpanded()) {
+                    && !ambientState.isShadeExpanded()
+                    && !skipClipBottomForCycling(child, ambientState)) {
                 // The bottom of this view is peeking out from under the previous view.
                 // Clip the part that is peeking out.
                 float overlapAmount = newNotificationEnd - firstHeadsUpEnd;
@@ -370,6 +375,44 @@
         }
     }
 
+    /**
+     * @return Should we skip clipping the bottom clipping when new hun has lower bottom line for
+     *         the hun cycling animation.
+     */
+    private boolean skipClipBottomForCycling(ExpandableView view, AmbientState ambientState) {
+        if (!NotificationHeadsUpCycling.isEnabled()) return false;
+        if (!isCyclingOut(view, ambientState)) return false;
+        // skip bottom clipping if we animate the bottom line
+        return NotificationHeadsUpCycling.getAnimateTallToShort();
+    }
+
+    /**
+     * Whether the view is the hun that is cycling out by the notification avalanche.
+     */
+    public boolean isCyclingOut(ExpandableView view, AmbientState ambientState) {
+        if (!NotificationHeadsUpCycling.isEnabled()) return false;
+        if (!(view instanceof ExpandableNotificationRow)) return false;
+        return isCyclingOut((ExpandableNotificationRow) view, ambientState);
+    }
+
+    /**
+     * Whether the row is the hun that is cycling out by the notification avalanche.
+     */
+    public boolean isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState) {
+        if (!NotificationHeadsUpCycling.isEnabled()) return false;
+        String cyclingOutKey = ambientState.getAvalanchePreviousHunKey();
+        return row.getEntry().getKey().equals(cyclingOutKey);
+    }
+
+    /**
+     * Whether the row is the hun that is cycling in by the notification avalanche.
+     */
+    public boolean isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState) {
+        if (!NotificationHeadsUpCycling.isEnabled()) return false;
+        String cyclingInKey = ambientState.getAvalancheShowingHunKey();
+        return row.getEntry().getKey().equals(cyclingInKey);
+    }
+
     /** Updates the dimmed and hiding sensitive states of the children. */
     private void updateDimmedAndHideSensitive(AmbientState ambientState,
             StackScrollAlgorithmState algorithmState) {
@@ -799,6 +842,7 @@
         }
 
         ExpandableNotificationRow topHeadsUpEntry = null;
+        int cyclingInHunHeight = -1;
         for (int i = 0; i < childCount; i++) {
             View child = algorithmState.visibleChildren.get(i);
             if (!(child instanceof ExpandableNotificationRow row)) {
@@ -839,6 +883,13 @@
                 childState.setYTranslation(
                         Math.max(childState.getYTranslation(), headsUpTranslation));
                 childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
+                if (NotificationHeadsUpCycling.isEnabled()) {
+                    if (isCyclingIn(row, ambientState)) {
+                        if (cyclingInHunHeight == -1) {
+                            cyclingInHunHeight = childState.height;
+                        }
+                    }
+                }
                 childState.hidden = false;
                 ExpandableViewState topState =
                         topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState();
@@ -860,6 +911,26 @@
                 }
             }
             if (row.isHeadsUpAnimatingAway()) {
+                if (NotificationHeadsUpCycling.isEnabled() && isCyclingOut(row, ambientState)) {
+                    // If the two HUNs in the cycling animation have different heights, we need
+                    // an extra y translation to align the animation.
+                    int extraTranslation;
+                    if (NotificationHeadsUpCycling.getAnimateTallToShort()) {
+                        if (cyclingInHunHeight > 0) {
+                            extraTranslation = cyclingInHunHeight - childState.height;
+                        } else {
+                            extraTranslation = 0;
+                        }
+                    } else {
+                        extraTranslation = cyclingInHunHeight >= childState.height
+                                ? cyclingInHunHeight - childState.height : 0;
+                    }
+                    extraTranslation += mHeadsUpCyclingPadding;
+                    float inSpaceTranslation = Math.max(childState.getYTranslation(),
+                            headsUpTranslation);
+                    childState.setYTranslation(inSpaceTranslation + extraTranslation);
+                    cyclingInHunHeight = -1;
+                } else
                 if (NotificationsImprovedHunAnimation.isEnabled() && !ambientState.isDozing()) {
                     if (shouldHunAppearFromBottom(ambientState, childState)) {
                         // move to the bottom of the screen
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 5963d35..5dc5449 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.stack;
 
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR;
+import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN;
+import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK;
 
@@ -57,6 +59,7 @@
     public static final int ANIMATION_DURATION_CLOSE_REMOTE_INPUT = 150;
     public static final int ANIMATION_DURATION_HEADS_UP_APPEAR = 400;
     public static final int ANIMATION_DURATION_HEADS_UP_DISAPPEAR = 400;
+    public static final int ANIMATION_DURATION_HEADS_UP_CYCLING = 400;
     public static final int ANIMATION_DURATION_FOLD_TO_AOD =
             AnimatableClockView.ANIMATION_DURATION_FOLD_TO_AOD;
     public static final int ANIMATION_DURATION_PRIORITY_CHANGE = 500;
@@ -68,6 +71,8 @@
 
     @VisibleForTesting int mGoToFullShadeAppearingTranslation;
     @VisibleForTesting float mHeadsUpAppearStartAboveScreen;
+    // Padding between the old and new heads up notifications for the hun cycling animation
+    private float mHeadsUpCyclingPadding;
     private final ExpandableViewState mTmpState = new ExpandableViewState();
     private final AnimationProperties mAnimationProperties;
     public NotificationStackScrollLayout mHostLayout;
@@ -125,6 +130,8 @@
                         R.dimen.go_to_full_shade_appearing_translation);
         mHeadsUpAppearStartAboveScreen = context.getResources()
                 .getDimensionPixelSize(R.dimen.heads_up_appear_y_above_screen);
+        mHeadsUpCyclingPadding = context.getResources()
+                .getDimensionPixelSize(R.dimen.heads_up_cycling_padding);
     }
 
     protected void setLogger(StackStateLogger logger) {
@@ -449,7 +456,8 @@
                 }
                 changingView.performRemoveAnimation(ANIMATION_DURATION_APPEAR_DISAPPEAR,
                         0 /* delay */, translationDirection, false /* isHeadsUpAppear */,
-                        startAnimation, postAnimation, getGlobalAnimationFinishedListener());
+                        startAnimation, postAnimation, getGlobalAnimationFinishedListener(),
+                        ExpandableView.ClipSide.BOTTOM);
                 needsCustomAnimation = true;
             } else if (event.animationType ==
                 NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) {
@@ -464,6 +472,27 @@
                     .AnimationEvent.ANIMATION_TYPE_GROUP_EXPANSION_CHANGED) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) event.mChangingView;
                 row.prepareExpansionChanged();
+            } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_IN) {
+                mHeadsUpAppearChildren.add(changingView);
+
+                mTmpState.copyFrom(changingView.getViewState());
+                mTmpState.setYTranslation(changingView.getViewState().getYTranslation()
+                        + getHeadsUpCyclingInYTranslationStart(event.headsUpFromBottom));
+                mTmpState.applyToView(changingView);
+
+                // TODO(b/339519404): use a different interpolator
+                Runnable onAnimationEnd = null;
+                if (loggable) {
+                    // This only captures HEADS_UP_APPEAR animations, but HUNs can appear with
+                    // normal ADD animations, which would not be logged here.
+                    String finalKey = key;
+                    mLogger.logHUNViewAppearing(key);
+                    onAnimationEnd = () -> {
+                        mLogger.appearAnimationEnded(finalKey);
+                    };
+                }
+                changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_CYCLING,
+                        /* isHeadsUpAppear= */ true, onAnimationEnd);
             } else if (NotificationsImprovedHunAnimation.isEnabled()
                     && (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR)) {
                 mHeadsUpAppearChildren.add(changingView);
@@ -486,6 +515,87 @@
                 }
                 changingView.performAddAnimation(0, ANIMATION_DURATION_HEADS_UP_APPEAR,
                         /* isHeadsUpAppear= */ true, onAnimationEnd);
+            } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_CYCLING_OUT) {
+                mHeadsUpDisappearChildren.add(changingView);
+                Runnable endRunnable = null;
+                mTmpState.copyFrom(changingView.getViewState());
+
+                if (changingView.getParent() == null) {
+                    // This notification was actually removed, so we need to add it
+                    // transiently
+                    mHostLayout.addTransientView(changingView, 0);
+                    changingView.setTransientContainer(mHostLayout);
+                    // TODO(b/316404716): remove the hard-coded height
+                    // StackScrollAlgorithm cannot find this view because it has been removed
+                    // from the NSSL. To correctly translate the view to the top or bottom of
+                    // the screen (where it animated from), we need to update its translation.
+                    mTmpState.setYTranslation(
+                            mTmpState.getYTranslation() + 10
+                    );
+                    endRunnable = changingView::removeFromTransientContainer;
+                }
+
+                boolean needsAnimation = true;
+                if (changingView instanceof ExpandableNotificationRow) {
+                    ExpandableNotificationRow row =
+                            (ExpandableNotificationRow) changingView;
+                    if (row.isDismissed()) {
+                        needsAnimation = false;
+                    }
+                }
+                if (needsAnimation) {
+                    // We need to add the global animation listener, since once no animations are
+                    // running anymore, the panel will instantly hide itself. We need to wait until
+                    // the animation is fully finished for this though.
+                    final Runnable tmpEndRunnable = endRunnable;
+                    Runnable postAnimation;
+                    Runnable startAnimation;
+                    if (loggable) {
+                        String finalKey1 = key;
+                        final boolean finalIsHeadsUp = isHeadsUp;
+                        final String type = "ANIMATION_TYPE_HEADS_UP_CYCLING_OUT";
+                        startAnimation = () -> {
+                            mLogger.animationStart(finalKey1, type, finalIsHeadsUp);
+                            changingView.setInRemovalAnimation(true);
+                        };
+                        postAnimation = () -> {
+                            mLogger.animationEnd(finalKey1, type, finalIsHeadsUp);
+                            changingView.setInRemovalAnimation(false);
+                            if (tmpEndRunnable != null) {
+                                tmpEndRunnable.run();
+                            }
+
+                        };
+                    } else {
+                        postAnimation = () -> {
+                            changingView.setInRemovalAnimation(false);
+                            if (tmpEndRunnable != null) {
+                                tmpEndRunnable.run();
+                            }
+                        };
+                        startAnimation = () -> {
+                            changingView.setInRemovalAnimation(true);
+                        };
+                    }
+                    long removeAnimationDelay = changingView.performRemoveAnimation(
+                            ANIMATION_DURATION_HEADS_UP_CYCLING,
+                            /* delay= */ 0,
+                            // It's a shame that translationDirection isn't where we do the y
+                            // translation, the actual translation is in StackScrollAlgorithm.
+                            /* translationDirection= */ 0.0f,
+                            /* isHeadsUpAnimation= */ true,
+                            startAnimation, postAnimation,
+                            getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.TOP);
+                    mAnimationProperties.delay += removeAnimationDelay;
+                    mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_CYCLING;
+                    mAnimationProperties.setCustomInterpolator(View.TRANSLATION_Y,
+                            Interpolators.LINEAR);
+                    mAnimationProperties.getAnimationFilter().animateY = true;
+                    mTmpState.animateTo(changingView, mAnimationProperties);
+                } else if (endRunnable != null) {
+                    endRunnable.run();
+                }
+                needsCustomAnimation |= needsAnimation;
             } else if (event.animationType == ANIMATION_TYPE_HEADS_UP_APPEAR) {
                 NotificationsImprovedHunAnimation.assertInLegacyMode();
                 // This item is added, initialize its properties.
@@ -565,21 +675,21 @@
                             }
                         };
                     } else {
+                        startAnimation = () -> {
+                            changingView.setInRemovalAnimation(true);
+                        };
                         postAnimation = () -> {
                             changingView.setInRemovalAnimation(false);
                             if (tmpEndRunnable != null) {
                                 tmpEndRunnable.run();
                             }
                         };
-                        startAnimation = () -> {
-                            changingView.setInRemovalAnimation(true);
-                        };
                     }
                     long removeAnimationDelay = changingView.performRemoveAnimation(
                             ANIMATION_DURATION_HEADS_UP_DISAPPEAR,
                             0, 0.0f, true /* isHeadsUpAppear */,
                             startAnimation, postAnimation,
-                            getGlobalAnimationFinishedListener());
+                            getGlobalAnimationFinishedListener(), ExpandableView.ClipSide.BOTTOM);
                     mAnimationProperties.delay += removeAnimationDelay;
                     if (NotificationsImprovedHunAnimation.isEnabled()) {
                         mAnimationProperties.duration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR;
@@ -607,6 +717,38 @@
         return -mStackTopMargin - mHeadsUpAppearStartAboveScreen;
     }
 
+    /**
+     * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen
+     * @return The start y translation of the HUN cycling in animation
+     */
+    private float getHeadsUpCyclingInYTranslationStart(boolean headsUpFromBottom) {
+        if (headsUpFromBottom) {
+            // start from the bottom of the screen
+            return mHeadsUpAppearHeightBottom + mHeadsUpCyclingPadding;
+        }
+        // start from the top of the screen
+        return -mHeadsUpCyclingPadding;
+    }
+
+    /**
+     * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen
+     * @param oldHunHeight Height of the old HUN
+     * @param newHunHeight Height of the new HUN
+     * @return The y translation target value of the HUN cycling out animation
+     */
+    private float getHeadsUpCyclingOutYTranslation(
+            boolean headsUpFromBottom,
+            int oldHunHeight,
+            int newHunHeight
+    ) {
+        final float translationDistance = mHeadsUpCyclingPadding + newHunHeight - oldHunHeight;
+        if (headsUpFromBottom) {
+            // start from the bottom of the screen
+            return mHeadsUpAppearHeightBottom - translationDistance;
+        }
+        return translationDistance;
+    }
+
     public void animateOverScrollToAmount(float targetAmount, final boolean onTop,
             final boolean isRubberbanded) {
         final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
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 1f1251a..0ba7b3c 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
@@ -68,8 +68,8 @@
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
-import com.android.systemui.util.kotlin.BooleanFlowOperators.and
-import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import javax.inject.Inject
@@ -246,7 +246,7 @@
                 keyguardTransitionInteractor.finishedKeyguardState.map { state ->
                     state == GLANCEABLE_HUB
                 },
-                or(
+                anyOf(
                     keyguardTransitionInteractor.isInTransitionToState(GLANCEABLE_HUB),
                     keyguardTransitionInteractor.isInTransitionFromState(GLANCEABLE_HUB),
                 ),
@@ -424,14 +424,14 @@
                 while (currentCoroutineContext().isActive) {
                     emit(false)
                     // Ensure states are inactive to start
-                    and(
+                    allOf(
                             *toFlowArray(statesForHiddenKeyguard) { state ->
                                 keyguardTransitionInteractor.transitionValue(state).map { it == 0f }
                             }
                         )
                         .first { it }
                     // Wait for a qualifying transition to begin
-                    or(
+                    anyOf(
                             *toFlowArray(statesForHiddenKeyguard) { state ->
                                 keyguardTransitionInteractor
                                     .transitionStepsToState(state)
@@ -446,7 +446,7 @@
                     // it is considered safe to reset alpha to 1f for HUNs.
                     combine(
                             keyguardInteractor.statusBarState,
-                            and(
+                            allOf(
                                 *toFlowArray(statesForHiddenKeyguard) { state ->
                                     keyguardTransitionInteractor.transitionValue(state).map {
                                         it == 0f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index be6bef7..23674b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2184,7 +2184,9 @@
         }
         if (mStatusBarStateController.leaveOpenOnKeyguardHide()) {
             if (!mStatusBarStateController.isKeyguardRequested()) {
-                mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
+                if (!MigrateClocksToBlueprint.isEnabled()) {
+                    mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
+                }
             }
             long delay = mKeyguardStateController.calculateGoingToFullShadeDelay();
             mLockscreenShadeTransitionController.onHideKeyguard(delay, previousState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index e1a7f22..e92058b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -96,6 +96,20 @@
 @SysUISingleton
 public class StatusBarNotificationActivityStarter implements NotificationActivityStarter {
 
+    /**
+     * Helps to avoid recalculation of values provided to
+     * {@link #onDismiss(PendingIntent, boolean, boolean, boolean)}} method
+     */
+    private interface OnKeyguardDismissedAction {
+        /**
+         * Invoked when keyguard is dismissed
+         *
+         * @return is used as return value for {@link ActivityStarter.OnDismissAction#onDismiss()}
+         */
+        boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animate,
+                boolean showOverTheLockScreen);
+    }
+
     private final Context mContext;
     private final int mDisplayId;
 
@@ -207,6 +221,30 @@
     }
 
     /**
+     * Called when the user clicks on the notification bubble icon.
+     *
+     * @param entry notification that bubble icon was clicked
+     */
+    @Override
+    public void onNotificationBubbleIconClicked(NotificationEntry entry) {
+        Runnable action = () -> {
+            mBubblesManagerOptional.ifPresent(bubblesManager ->
+                    bubblesManager.onUserChangedBubble(entry, !entry.isBubble()));
+            mHeadsUpManager.removeNotification(entry.getKey(), /* releaseImmediately= */ true);
+        };
+        if (entry.isBubble()) {
+            // entry is being un-bubbled, no need to unlock
+            action.run();
+        } else {
+            performActionAfterKeyguardDismissed(entry,
+                    (intent, isActivityIntent, animate, showOverTheLockScreen) -> {
+                        action.run();
+                        return false;
+                    });
+        }
+    }
+
+    /**
      * Called when a notification is clicked.
      *
      * @param entry notification that was clicked
@@ -217,7 +255,15 @@
         mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(),
                 mKeyguardStateController.isVisible(),
                 mNotificationShadeWindowController.getPanelExpanded());
+        OnKeyguardDismissedAction action =
+                (intent, isActivityIntent, animate, showOverTheLockScreen) ->
+                        performActionOnKeyguardDismissed(entry, row, intent, isActivityIntent,
+                                animate, showOverTheLockScreen);
+        performActionAfterKeyguardDismissed(entry, action);
+    }
 
+    private void performActionAfterKeyguardDismissed(NotificationEntry entry,
+            OnKeyguardDismissedAction action) {
         if (mRemoteInputManager.isRemoteInputActive(entry)) {
             // We have an active remote input typed and the user clicked on the notification.
             // this was probably unintentional, so we're closing the edit text instead.
@@ -251,8 +297,7 @@
         ActivityStarter.OnDismissAction postKeyguardAction = new ActivityStarter.OnDismissAction() {
             @Override
             public boolean onDismiss() {
-                return handleNotificationClickAfterKeyguardDismissed(
-                        entry, row, intent, isActivityIntent, animate, showOverLockscreen);
+                return action.onDismiss(intent, isActivityIntent, animate, showOverLockscreen);
             }
 
             @Override
@@ -271,7 +316,7 @@
         }
     }
 
-    private boolean handleNotificationClickAfterKeyguardDismissed(
+    private boolean performActionOnKeyguardDismissed(
             NotificationEntry entry,
             ExpandableNotificationRow row,
             PendingIntent intent,
@@ -282,7 +327,6 @@
 
         final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
                 entry, row, intent, isActivityIntent, animate);
-
         if (showOverLockscreen) {
             mShadeController.addPostCollapseAction(runnable);
             mShadeController.collapseShade(true /* animate */);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 226a84a..88ca9e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -229,7 +229,7 @@
         @SysUISingleton
         @OemSatelliteInputLog
         fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer {
-            return factory.create("DeviceBasedSatelliteInputLog", 32)
+            return factory.create("DeviceBasedSatelliteInputLog", 150)
         }
 
         const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
index d9d909a..fc54f14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
@@ -33,6 +33,18 @@
      */
     val isOpportunistic: Boolean = false,
 
+    /**
+     * True if this subscription **only** supports non-terrestrial networks (NTN) and false
+     * otherwise. (non-terrestrial == satellite)
+     *
+     * Note that we intend to filter these subscriptions out, because these connections are actually
+     * supported by
+     * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. See
+     * [com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor] for
+     * the filtering.
+     */
+    val isExclusivelyNonTerrestrial: Boolean = false,
+
     /** Subscriptions in the same group may be filtered or treated as a single subscription */
     val groupUuid: ParcelUuid? = null,
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 2278597..425c58b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import kotlinx.coroutines.flow.StateFlow
 
@@ -76,7 +77,17 @@
      */
     val isInService: StateFlow<Boolean>
 
-    /** Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork] */
+    /**
+     * True if this subscription is actively connected to a non-terrestrial network and false
+     * otherwise. Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork].
+     *
+     * Notably: This value reflects that this subscription is **currently** using a non-terrestrial
+     * network, because some subscriptions can switch between terrestrial and non-terrestrial
+     * networks. [SubscriptionModel.isExclusivelyNonTerrestrial] reflects whether a subscription is
+     * configured to exclusively connect to non-terrestrial networks. [isNonTerrestrial] can change
+     * during the lifetime of a subscription but [SubscriptionModel.isExclusivelyNonTerrestrial]
+     * will stay constant.
+     */
     val isNonTerrestrial: StateFlow<Boolean>
 
     /** True if [android.telephony.SignalStrength] told us that this connection is using GSM */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 5d91ef3..0073e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -424,6 +424,7 @@
         SubscriptionModel(
             subscriptionId = subscriptionId,
             isOpportunistic = isOpportunistic,
+            isExclusivelyNonTerrestrial = isOnlyNonTerrestrialNetwork,
             groupUuid = groupUuid,
             carrierName = carrierName.toString(),
             profileClass = profileClass,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index d555c47..91d7ca6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -172,21 +172,33 @@
     private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
         mobileConnectionsRepo.subscriptions
 
-    /**
-     * Any filtering that we can do based purely on the info of each subscription. Currently this
-     * only applies the ProfileClass-based filter, but if we need other they can go here
-     */
+    /** Any filtering that we can do based purely on the info of each subscription individually. */
     private val subscriptionsBasedFilteredSubs =
-        unfilteredSubscriptions.map { subs -> applyProvisioningFilter(subs) }.distinctUntilChanged()
+        unfilteredSubscriptions
+            .map { it.filterBasedOnProvisioning().filterBasedOnNtn() }
+            .distinctUntilChanged()
 
-    private fun applyProvisioningFilter(subs: List<SubscriptionModel>): List<SubscriptionModel> =
+    private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> =
         if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
-            subs
+            this
         } else {
-            subs.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
+            this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
         }
 
     /**
+     * Subscriptions that exclusively support non-terrestrial networks should **never** directly
+     * show any iconography in the status bar. These subscriptions only exist to provide a backing
+     * for the device-based satellite connections, and the iconography for those connections are
+     * already being handled in
+     * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We
+     * need to filter out those subscriptions here so we guarantee the subscription never turns into
+     * an icon. See b/336881301.
+     */
+    private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> {
+        return this.filter { !it.isExclusivelyNonTerrestrial }
+    }
+
+    /**
      * Generally, SystemUI wants to show iconography for each subscription that is listed by
      * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
      * show a single representation of the pair of subscriptions. The docs define opportunistic as:
@@ -204,12 +216,8 @@
                 subscriptionsBasedFilteredSubs,
                 mobileConnectionsRepo.activeMobileDataSubscriptionId,
                 connectivityRepository.vcnSubId,
-            ) { unfilteredSubs, activeId, vcnSubId ->
-                filterSubsBasedOnOpportunistic(
-                    unfilteredSubs,
-                    activeId,
-                    vcnSubId,
-                )
+            ) { preFilteredSubs, activeId, vcnSubId ->
+                filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
             }
             .distinctUntilChanged()
             .logDiffsForTable(
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 a7c4187..12f252d 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
@@ -249,11 +249,17 @@
                 try {
                     sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
                     registered = true
+                    logBuffer.i { "Registered for signal strength successfully" }
                 } catch (e: Exception) {
                     logBuffer.e("error registering for signal strength", e)
                 }
 
-                awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) }
+                awaitClose {
+                    if (registered) {
+                        sm.unregisterForNtnSignalStrengthChanged(cb)
+                        logBuffer.i { "Unregistered for signal strength successfully" }
+                    }
+                }
             }
             .flowOn(bgDispatcher)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 2670a95..fa8a7d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -253,6 +253,7 @@
 
         if (nextList.isEmpty()) {
             log { "NO MORE TO SHOW" }
+            previousHunKey = ""
             return
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 37be1c6..a817b31 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.user.data.repository
 
 import android.annotation.SuppressLint
+import android.annotation.UserIdInt
 import android.content.Context
 import android.content.pm.UserInfo
 import android.os.UserHandle
@@ -107,6 +108,22 @@
     fun isSimpleUserSwitcher(): Boolean
 
     fun isUserSwitcherEnabled(): Boolean
+
+    /**
+     * Returns the user ID of the "main user" of the device. This user may have access to certain
+     * features which are limited to at most one user. There will never be more than one main user
+     * on a device.
+     *
+     * <p>Currently, on most form factors the first human user on the device will be the main user;
+     * in the future, the concept may be transferable, so a different user (or even no user at all)
+     * may be designated the main user instead. On other form factors there might not be a main
+     * user.
+     *
+     * <p> When the device doesn't have a main user, this will return {@code null}.
+     *
+     * @see [UserManager.getMainUser]
+     */
+    @UserIdInt suspend fun getMainUserId(): Int?
 }
 
 @SysUISingleton
@@ -239,6 +256,10 @@
         return _userSwitcherSettings.value.isUserSwitcherEnabled
     }
 
+    override suspend fun getMainUserId(): Int? {
+        return withContext(backgroundDispatcher) { manager.mainUser?.identifier }
+    }
+
     private suspend fun getSettings(): UserSwitcherSettingsModel {
         return withContext(backgroundDispatcher) {
             val isSimpleUserSwitcher =
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
index 38b381a..59c819d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
@@ -2,6 +2,7 @@
 
 import android.annotation.UserIdInt
 import android.content.pm.UserInfo
+import android.os.UserManager
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.Flags.refactorGetCurrentUser
 import com.android.systemui.dagger.SysUISingleton
@@ -38,4 +39,23 @@
             KeyguardUpdateMonitor.getCurrentUser()
         }
     }
+
+    /**
+     * Returns the user ID of the "main user" of the device. This user may have access to certain
+     * features which are limited to at most one user. There will never be more than one main user
+     * on a device.
+     *
+     * <p>Currently, on most form factors the first human user on the device will be the main user;
+     * in the future, the concept may be transferable, so a different user (or even no user at all)
+     * may be designated the main user instead. On other form factors there might not be a main
+     * user.
+     *
+     * <p> When the device doesn't have a main user, this will return {@code null}.
+     *
+     * @see [UserManager.getMainUser]
+     */
+    @UserIdInt
+    fun getMainUserId(): Int? {
+        return repository.mainUserId
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
index b300885..a2759c6 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
@@ -28,11 +28,23 @@
      *
      * Usage:
      * ```
-     * val result = and(flow1, flow2)
+     * val result = allOf(flow1, flow2)
      * ```
      */
-    fun and(vararg flows: Flow<Boolean>): Flow<Boolean> =
-        combine(flows.asIterable()) { values -> values.all { it } }.distinctUntilChanged()
+    fun allOf(vararg flows: Flow<Boolean>): Flow<Boolean> = flows.asIterable().all()
+
+    /**
+     * Logical AND operator for boolean flows. Will collect all flows and [combine] them to
+     * determine the result.
+     */
+    fun Array<Flow<Boolean>>.all(): Flow<Boolean> = allOf(*this)
+
+    /**
+     * Logical AND operator for boolean flows. Will collect all flows and [combine] them to
+     * determine the result.
+     */
+    fun Iterable<Flow<Boolean>>.all(): Flow<Boolean> =
+        combine(this) { values -> values.all { it } }.distinctUntilChanged()
 
     /**
      * Logical NOT operator for a boolean flow.
@@ -48,6 +60,36 @@
      * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
      * determine the result.
      */
-    fun or(vararg flows: Flow<Boolean>): Flow<Boolean> =
-        combine(flows.asIterable()) { values -> values.any { it } }.distinctUntilChanged()
+    fun anyOf(vararg flows: Flow<Boolean>): Flow<Boolean> = flows.asIterable().any()
+
+    /**
+     * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
+     * determine the result.
+     */
+    fun Array<Flow<Boolean>>.any(): Flow<Boolean> = anyOf(*this)
+
+    /**
+     * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
+     * determine the result.
+     */
+    fun Iterable<Flow<Boolean>>.any(): Flow<Boolean> =
+        combine(this) { values -> values.any { it } }.distinctUntilChanged()
+
+    /**
+     * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise
+     * produces `false`.
+     */
+    fun noneOf(vararg flows: Flow<Boolean>): Flow<Boolean> = not(anyOf(*flows))
+
+    /**
+     * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise
+     * produces `false`.
+     */
+    fun Array<Flow<Boolean>>.none(): Flow<Boolean> = noneOf(*this)
+
+    /**
+     * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise
+     * produces `false`.
+     */
+    fun Iterable<Flow<Boolean>>.none(): Flow<Boolean> = not(any())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
new file mode 100644
index 0000000..ee1b565
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+fun LocationController.isLocationEnabledFlow(): Flow<Boolean> {
+    return conflatedCallbackFlow {
+            val locationCallback =
+                object : LocationController.LocationChangeCallback {
+                    override fun onLocationSettingsChanged(locationEnabled: Boolean) {
+                        trySend(locationEnabled)
+                    }
+                }
+            addCallback(locationCallback)
+            awaitClose { removeCallback(locationCallback) }
+        }
+        .onStart { emit(isLocationEnabled) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 0386338..c08cd64 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -49,6 +49,11 @@
     private val uiEventLogger: UiEventLogger,
 ) : SliderViewModel {
 
+    private val streamsAffectedByRing =
+        setOf(
+            AudioManager.STREAM_RING,
+            AudioManager.STREAM_NOTIFICATION,
+        )
     private val audioStream = audioStreamWrapper.audioStream
     private val iconsByStream =
         mapOf(
@@ -125,15 +130,42 @@
         isEnabled: Boolean,
         ringerMode: RingerMode,
     ): State {
+        val label =
+            labelsByStream[audioStream]?.let(context::getString)
+                ?: error("No label for the stream: $audioStream")
         return State(
             value = volume.toFloat(),
             valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
             icon = getIcon(ringerMode),
-            label = labelsByStream[audioStream]?.let(context::getString)
-                    ?: error("No label for the stream: $audioStream"),
+            label = label,
             disabledMessage = disabledTextByStream[audioStream]?.let(context::getString),
             isEnabled = isEnabled,
             a11yStep = volumeRange.step,
+            a11yClickDescription =
+                context.getString(
+                    if (isMuted) {
+                        R.string.volume_panel_hint_unmute
+                    } else {
+                        R.string.volume_panel_hint_mute
+                    },
+                    label,
+                ),
+            a11yStateDescription =
+                if (volume == volumeRange.first) {
+                    context.getString(
+                        if (audioStream.value in streamsAffectedByRing) {
+                            if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
+                                R.string.volume_panel_hint_vibrate
+                            } else {
+                                R.string.volume_panel_hint_muted
+                            }
+                        } else {
+                            R.string.volume_panel_hint_muted
+                        }
+                    )
+                } else {
+                    null
+                },
             audioStreamModel = this,
             isMutable = audioVolumeInteractor.isAffectedByMute(audioStream),
         )
@@ -143,27 +175,14 @@
         val isMutedOrNoVolume = isMuted || volume == minVolume
         val iconRes =
             if (isMutedOrNoVolume) {
-                when (audioStream.value) {
-                    AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_off
-                    AudioManager.STREAM_BLUETOOTH_SCO -> R.drawable.ic_volume_off
-                    AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_volume_off
-                    AudioManager.STREAM_RING ->
-                        if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
-                            R.drawable.ic_volume_ringer_vibrate
-                        } else {
-                            R.drawable.ic_volume_off
-                        }
-                    AudioManager.STREAM_NOTIFICATION ->
-                        if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
-                            R.drawable.ic_volume_ringer_vibrate
-                        } else {
-                            R.drawable.ic_volume_off
-                        }
-                    AudioManager.STREAM_ALARM -> R.drawable.ic_volume_off
-                    else -> {
-                        Log.wtf(TAG, "No icon for the stream: $audioStream")
+                if (audioStream.value in streamsAffectedByRing) {
+                    if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
+                        R.drawable.ic_volume_ringer_vibrate
+                    } else {
                         R.drawable.ic_volume_off
                     }
+                } else {
+                    R.drawable.ic_volume_off
                 }
             } else {
                 iconsByStream[audioStream]
@@ -186,6 +205,8 @@
         override val disabledMessage: String?,
         override val isEnabled: Boolean,
         override val a11yStep: Int,
+        override val a11yClickDescription: String?,
+        override val a11yStateDescription: String?,
         override val isMutable: Boolean,
         val audioStreamModel: AudioStreamModel,
     ) : SliderState
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 956ab66..10714d1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -68,7 +68,7 @@
             icon = Icon.Resource(R.drawable.ic_cast, null),
             label = context.getString(R.string.media_device_cast),
             isEnabled = true,
-            a11yStep = 1
+            a11yStep = 1,
         )
     }
 
@@ -85,6 +85,12 @@
 
         override val isMutable: Boolean
             get() = false
+
+        override val a11yClickDescription: String?
+            get() = null
+
+        override val a11yStateDescription: String?
+            get() = null
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index d71a9d8..c951928 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -34,6 +34,8 @@
      * enough to trigger rounding to the correct value.
      */
     val a11yStep: Int
+    val a11yClickDescription: String?
+    val a11yStateDescription: String?
     val disabledMessage: String?
     val isMutable: Boolean
 
@@ -44,6 +46,8 @@
         override val label: String = ""
         override val disabledMessage: String? = null
         override val a11yStep: Int = 0
+        override val a11yClickDescription: String? = null
+        override val a11yStateDescription: String? = null
         override val isEnabled: Boolean = true
         override val isMutable: Boolean = false
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index e72027a..6f550ba 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -371,7 +371,7 @@
         }
 
     @Test
-    fun listenForTransitionToLSFromOccluded_updatesClockDozeAmountToOne() =
+    fun listenForTransitionToLSFromOccluded_updatesClockDozeAmountToZero() =
         runBlocking(IMMEDIATE) {
             val transitionStep = MutableStateFlow(TransitionStep())
             whenever(keyguardTransitionInteractor.transitionStepsToState(KeyguardState.LOCKSCREEN))
@@ -434,6 +434,27 @@
         }
 
     @Test
+    fun listenForAnyStateToDozingTransition_UpdatesClockDozeAmountToOne() =
+        runBlocking(IMMEDIATE) {
+            val transitionStep = MutableStateFlow(TransitionStep())
+            whenever(keyguardTransitionInteractor.transitionStepsToState(KeyguardState.DOZING))
+                    .thenReturn(transitionStep)
+
+            val job = underTest.listenForAnyStateToDozingTransition(this)
+            transitionStep.value =
+                    TransitionStep(
+                            from = KeyguardState.LOCKSCREEN,
+                            to = KeyguardState.DOZING,
+                            transitionState = TransitionState.STARTED,
+                    )
+            yield()
+
+            verify(animations, times(2)).doze(1f)
+
+            job.cancel()
+        }
+
+    @Test
     fun unregisterListeners_validate() =
         runBlocking(IMMEDIATE) {
             underTest.unregisterListeners()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
index 25e5470..3164f8e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.accessibility;
 
+import static com.android.systemui.accessibility.Magnification.DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -23,11 +25,17 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.display.DisplayManager;
 import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -39,6 +47,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
@@ -47,6 +56,7 @@
 import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -58,9 +68,12 @@
  */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class IMagnificationConnectionTest extends SysuiTestCase {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY;
     @Mock
     private AccessibilityManager mAccessibilityManager;
@@ -90,6 +103,7 @@
     private IMagnificationConnection mIMagnificationConnection;
     private Magnification mMagnification;
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+    private TestableLooper mTestableLooper;
 
     @Before
     public void setUp() throws Exception {
@@ -100,8 +114,10 @@
             return null;
         }).when(mAccessibilityManager).setMagnificationConnection(
                 any(IMagnificationConnection.class));
+        mTestableLooper = TestableLooper.get(this);
+        assertNotNull(mTestableLooper);
         mMagnification = new Magnification(getContext(),
-                getContext().getMainThreadHandler(), getContext().getMainExecutor(), mCommandQueue,
+                mTestableLooper.getLooper(), getContext().getMainExecutor(), mCommandQueue,
                 mModeSwitchesController, mSysUiState, mOverviewProxyService, mSecureSettings,
                 mDisplayTracker, getContext().getSystemService(DisplayManager.class), mA11yLogger);
         mMagnification.mWindowMagnificationControllerSupplier =
@@ -122,7 +138,7 @@
     public void enableWindowMagnification_passThrough() throws RemoteException {
         mIMagnificationConnection.enableWindowMagnification(TEST_DISPLAY, 3.0f, Float.NaN,
                 Float.NaN, 0f, 0f, mAnimationCallback);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mWindowMagnificationController).enableWindowMagnification(eq(3.0f),
                 eq(Float.NaN), eq(Float.NaN), eq(0f), eq(0f), eq(mAnimationCallback));
@@ -131,7 +147,7 @@
     @Test
     public void onFullscreenMagnificationActivationChanged_passThrough() throws RemoteException {
         mIMagnificationConnection.onFullscreenMagnificationActivationChanged(TEST_DISPLAY, true);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mFullscreenMagnificationController)
                 .onFullscreenMagnificationActivationChanged(eq(true));
@@ -141,7 +157,7 @@
     public void disableWindowMagnification_deleteWindowMagnification() throws RemoteException {
         mIMagnificationConnection.disableWindowMagnification(TEST_DISPLAY,
                 mAnimationCallback);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mWindowMagnificationController).deleteWindowMagnification(
                 mAnimationCallback);
@@ -150,7 +166,7 @@
     @Test
     public void setScaleForWindowMagnification() throws RemoteException {
         mIMagnificationConnection.setScaleForWindowMagnification(TEST_DISPLAY, 3.0f);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mWindowMagnificationController).setScale(3.0f);
     }
@@ -158,7 +174,7 @@
     @Test
     public void moveWindowMagnifier() throws RemoteException {
         mIMagnificationConnection.moveWindowMagnifier(TEST_DISPLAY, 100f, 200f);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mWindowMagnificationController).moveWindowMagnifier(100f, 200f);
     }
@@ -167,37 +183,102 @@
     public void moveWindowMagnifierToPosition() throws RemoteException {
         mIMagnificationConnection.moveWindowMagnifierToPosition(TEST_DISPLAY,
                 100f, 200f, mAnimationCallback);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mWindowMagnificationController).moveWindowMagnifierToPosition(
                 eq(100f), eq(200f), any(IRemoteMagnificationAnimationCallback.class));
     }
 
     @Test
-    public void showMagnificationButton() throws RemoteException {
+    @RequiresFlagsDisabled(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON)
+    public void showMagnificationButton_flagOff_directlyShowButton() throws RemoteException {
         // magnification settings panel should not be showing
         assertFalse(mMagnification.isMagnificationSettingsPanelShowing(TEST_DISPLAY));
 
         mIMagnificationConnection.showMagnificationButton(TEST_DISPLAY,
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mModeSwitchesController).showButton(TEST_DISPLAY,
                 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON)
+    public void showMagnificationButton_flagOn_delayedShowButton() throws RemoteException {
+        // magnification settings panel should not be showing
+        assertFalse(mMagnification.isMagnificationSettingsPanelShowing(TEST_DISPLAY));
+
+        mIMagnificationConnection.showMagnificationButton(TEST_DISPLAY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        // This processAllPendingMessages lets the IMagnificationConnection to delegate the
+        // showMagnificationButton request to Magnification.
+        processAllPendingMessages();
+
+        // The delayed message would be processed after DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS.
+        // So call this processAllPendingMessages with a timeout to verify the showButton
+        // will be called.
+        int timeout = DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS + 100;
+        processAllPendingMessages(timeout);
+        verify(mModeSwitchesController).showButton(TEST_DISPLAY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+    }
+
+    @Test
+    public void showMagnificationButton_settingsPanelShowing_doNotShowButton()
+            throws RemoteException {
+        when(mMagnificationSettingsController.isMagnificationSettingsShowing()).thenReturn(true);
+
+        mIMagnificationConnection.showMagnificationButton(TEST_DISPLAY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        // This processAllPendingMessages lets the IMagnificationConnection to delegate the
+        // showMagnificationButton request to Magnification.
+        processAllPendingMessages();
+
+        // If the flag is on, the isMagnificationSettingsShowing will be checked after timeout, so
+        // process all message after a timeout here to verify the showButton will not be called.
+        int timeout = Flags.delayShowMagnificationButton()
+                ? DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS + 100
+                : 0;
+        processAllPendingMessages(timeout);
+        verify(mModeSwitchesController, never()).showButton(TEST_DISPLAY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+    }
+
+    @Test
     public void removeMagnificationButton() throws RemoteException {
         mIMagnificationConnection.removeMagnificationButton(TEST_DISPLAY);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mModeSwitchesController).removeButton(TEST_DISPLAY);
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DELAY_SHOW_MAGNIFICATION_BUTTON)
+    public void removeMagnificationButton_delayingShowButton_doNotShowButtonAfterTimeout()
+            throws RemoteException {
+        // magnification settings panel should not be showing
+        assertFalse(mMagnification.isMagnificationSettingsPanelShowing(TEST_DISPLAY));
+
+        mIMagnificationConnection.showMagnificationButton(TEST_DISPLAY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+        mIMagnificationConnection.removeMagnificationButton(TEST_DISPLAY);
+        // This processAllPendingMessages lets the IMagnificationConnection to delegate the
+        // requests to Magnification.
+        processAllPendingMessages();
+
+        // Call this processAllPendingMessages with a timeout to ensure the delayed show button
+        // message should be removed and thus the showButton will not be called after timeout.
+        int timeout = DELAY_SHOW_MAGNIFICATION_TIMEOUT_MS + 100;
+        processAllPendingMessages(/* timeForwardMs= */ timeout);
+        verify(mModeSwitchesController, never()).showButton(TEST_DISPLAY,
+                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
+    }
+
+    @Test
     public void removeMagnificationSettingsPanel() throws RemoteException {
         mIMagnificationConnection.removeMagnificationSettingsPanel(TEST_DISPLAY);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         verify(mMagnificationSettingsController).closeMagnificationSettings();
     }
@@ -208,7 +289,7 @@
         final float testScale = 3.0f;
         mIMagnificationConnection.onUserMagnificationScaleChanged(
                 testUserId, TEST_DISPLAY, testScale);
-        waitForIdleSync();
+        processAllPendingMessages();
 
         assertTrue(mMagnification.mUsersScales.contains(testUserId));
         assertEquals(mMagnification.mUsersScales.get(testUserId).get(TEST_DISPLAY),
@@ -216,6 +297,17 @@
         verify(mMagnificationSettingsController).setMagnificationScale(eq(testScale));
     }
 
+    private void processAllPendingMessages() {
+        processAllPendingMessages(/* timeForwardMs=*/ 0);
+    }
+
+    private void processAllPendingMessages(int timeForwardMs) {
+        if (timeForwardMs > 0) {
+            mTestableLooper.moveTimeForward(timeForwardMs);
+        }
+        mTestableLooper.processAllMessages();
+    }
+
     private class FakeWindowMagnificationControllerSupplier extends
             DisplayIdIndexSupplier<WindowMagnificationController> {
 
@@ -229,7 +321,6 @@
         }
     }
 
-
     private class FakeFullscreenMagnificationControllerSupplier extends
             DisplayIdIndexSupplier<FullscreenMagnificationController> {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 41974f4..8e4c155 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -46,7 +46,8 @@
 @RunWithLooper
 class ActivityTransitionAnimatorTest : SysuiTestCase() {
     private val transitionContainer = LinearLayout(mContext)
-    private val testTransitionAnimator = fakeTransitionAnimator()
+    private val mainExecutor = context.mainExecutor
+    private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
     @Mock lateinit var callback: ActivityTransitionAnimator.Callback
     @Mock lateinit var listener: ActivityTransitionAnimator.Listener
     @Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@@ -59,9 +60,10 @@
     fun setup() {
         activityTransitionAnimator =
             ActivityTransitionAnimator(
+                mainExecutor,
                 testTransitionAnimator,
                 testTransitionAnimator,
-                disableWmTimeout = true
+                disableWmTimeout = true,
             )
         activityTransitionAnimator.callback = callback
         activityTransitionAnimator.addListener(listener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
index d84a578..e14762cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
@@ -156,6 +156,7 @@
     fun testActivityLaunchWhenLockedWithoutAlternateAuth() {
         val dialogTransitionAnimator =
                 fakeDialogTransitionAnimator(
+                        mainExecutor = mContext.mainExecutor,
                         isUnlocked = false,
                         isShowingAlternateAuthOnUnlock = false,
                         interactionJankMonitor = kosmos.interactionJankMonitor)
@@ -166,6 +167,7 @@
     @Test
     fun testActivityLaunchWhenLockedWithAlternateAuth() {
         val dialogTransitionAnimator = fakeDialogTransitionAnimator(
+                mainExecutor = mContext.mainExecutor,
                 isUnlocked = false,
                 isShowingAlternateAuthOnUnlock = true,
                 interactionJankMonitor = kosmos.interactionJankMonitor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index e64df90..259ece9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -25,6 +25,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.activity.EmptyTestActivity
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -58,9 +60,11 @@
             )
     }
 
+    private val kosmos = Kosmos()
     private val pathManager = GoldenPathManager(context, GOLDENS_PATH, pathConfig = PathConfig())
     private val transitionAnimator =
         TransitionAnimator(
+            kosmos.fakeExecutor,
             ActivityTransitionAnimator.TIMINGS,
             ActivityTransitionAnimator.INTERPOLATORS
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
index 2172bc5..8695c01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
@@ -5,12 +5,12 @@
 import android.hardware.biometrics.PromptVerticalListContentView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
 import com.android.systemui.biometrics.promptInfo
 import com.android.systemui.biometrics.shared.model.BiometricUserInfo
+import com.android.systemui.biometrics.shared.model.PromptKind
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
@@ -110,7 +110,7 @@
                     it.description = description
                     it.subtitle = subtitle
                 },
-                kind = Utils.CREDENTIAL_PIN,
+                kind = PromptKind.Pin,
                 userId = USER_ID,
                 challenge = OPERATION_ID,
                 opPackageName = OP_PACKAGE_NAME
@@ -135,7 +135,7 @@
                     it.subtitle = subtitle
                     it.contentView = contentView
                 },
-                kind = Utils.CREDENTIAL_PIN,
+                kind = PromptKind.Pin,
                 userId = USER_ID,
                 challenge = OPERATION_ID,
                 opPackageName = OP_PACKAGE_NAME
@@ -163,7 +163,7 @@
                     it.subtitle = subtitle
                     it.contentView = contentView
                 },
-                kind = Utils.CREDENTIAL_PIN,
+                kind = PromptKind.Pin,
                 userId = USER_ID,
                 challenge = OPERATION_ID,
                 opPackageName = OP_PACKAGE_NAME
@@ -171,13 +171,13 @@
             assertThat(showTitleOnly).isFalse()
         }
 
-    @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN)
+    @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(PromptKind.Pin)
 
-    @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD)
+    @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(PromptKind.Password)
 
-    @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN)
+    @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(PromptKind.Pattern)
 
-    private fun useCredentialForPrompt(kind: Int) =
+    private fun useCredentialForPrompt(kind: PromptKind) =
         testScope.runTest {
             val isStealth = false
             credentialInteractor.stealthMode = isStealth
@@ -211,11 +211,10 @@
             assertThat(prompt)
                 .isInstanceOf(
                     when (kind) {
-                        Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java
-                        Utils.CREDENTIAL_PASSWORD ->
+                        PromptKind.Pin -> BiometricPromptRequest.Credential.Pin::class.java
+                        PromptKind.Password ->
                             BiometricPromptRequest.Credential.Password::class.java
-                        Utils.CREDENTIAL_PATTERN ->
-                            BiometricPromptRequest.Credential.Pattern::class.java
+                        PromptKind.Pattern -> BiometricPromptRequest.Credential.Pattern::class.java
                         else -> throw Exception("wrong kind")
                     }
                 )
@@ -341,6 +340,28 @@
 
             job.cancel()
         }
+
+    /** Update the current request to use credential-based authentication instead of biometrics. */
+    private fun PromptCredentialInteractor.useCredentialsForAuthentication(
+        promptInfo: PromptInfo,
+        kind: PromptKind,
+        userId: Int,
+        challenge: Long,
+        opPackageName: String,
+    ) {
+        biometricPromptRepository.setPrompt(
+            promptInfo,
+            userId,
+            challenge,
+            kind,
+            opPackageName,
+        )
+    }
+
+    /** Unset the current authentication request. */
+    private fun PromptCredentialInteractor.resetPrompt() {
+        biometricPromptRepository.unsetPrompt()
+    }
 }
 
 private fun pinRequest(): BiometricPromptRequest.Credential.Pin =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
index 2817780..c308507 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -22,7 +22,6 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.Utils
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.FakePromptRepository
 import com.android.systemui.biometrics.faceSensorPropertiesInternal
@@ -143,21 +142,20 @@
     }
 
     @Test
-    fun usePinCredentialAndReset() =
-        testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PIN) }
+    fun usePinCredentialAndReset() = testScope.runTest { useCredentialAndReset(PromptKind.Pin) }
 
     @Test
     fun usePatternCredentialAndReset() =
-        testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PATTERN) }
+        testScope.runTest { useCredentialAndReset(PromptKind.Pattern) }
 
     @Test
     fun usePasswordCredentialAndReset() =
-        testScope.runTest { useCredentialAndReset(Utils.CREDENTIAL_PASSWORD) }
+        testScope.runTest { useCredentialAndReset(PromptKind.Password) }
 
-    private fun TestScope.useCredentialAndReset(@Utils.CredentialType kind: Int) {
+    private fun TestScope.useCredentialAndReset(kind: PromptKind) {
         setUserCredentialType(
-            isPin = kind == Utils.CREDENTIAL_PIN,
-            isPassword = kind == Utils.CREDENTIAL_PASSWORD,
+            isPin = kind == PromptKind.Pin,
+            isPassword = kind == PromptKind.Password,
         )
 
         val info =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt
index 5caa146..0d01472 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt
@@ -16,97 +16,95 @@
 
 package com.android.systemui.biometrics.ui.viewmodel
 
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.flags.parameterizeSceneContainerFlag
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.user.domain.UserDomainLayerModule
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.statusbar.phone.systemUIDialogManager
+import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
-class DefaultUdfpsTouchOverlayViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class DefaultUdfpsTouchOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply {
+            fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
+        }
+    private val testScope = kosmos.testScope
+
     @Captor
     private lateinit var sysuiDialogListenerCaptor: ArgumentCaptor<SystemUIDialogManager.Listener>
-    private var systemUIDialogManager: SystemUIDialogManager = mock()
+    private var systemUIDialogManager = kosmos.systemUIDialogManager
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
+
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+    private lateinit var underTest: DefaultUdfpsTouchOverlayViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return parameterizeSceneContainerFlag()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        underTest =
+            DefaultUdfpsTouchOverlayViewModel(
+                kosmos.shadeInteractor,
+                systemUIDialogManager,
+            )
     }
 
-    @SysUISingleton
-    @Component(
-        modules =
-            [
-                SysUITestModule::class,
-                UserDomainLayerModule::class,
-                BiometricsDomainLayerModule::class,
-            ]
-    )
-    interface TestComponent : SysUITestComponent<DefaultUdfpsTouchOverlayViewModel> {
-        val keyguardRepository: FakeKeyguardRepository
-        val shadeRepository: FakeShadeRepository
-        @Component.Factory
-        interface Factory {
-            fun create(
-                @BindsInstance test: SysuiTestCase,
-                featureFlags: FakeFeatureFlagsClassicModule,
-                mocks: TestMocksModule,
-            ): TestComponent
-        }
-    }
-
-    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+    private fun shadeExpanded(expanded: Boolean) {
         if (expanded) {
-            shadeRepository.setLegacyShadeExpansion(1f)
-            shadeRepository.setLegacyShadeTracking(false)
-            shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(true)
+            shadeTestUtil.setShadeExpansion(1f)
+            shadeTestUtil.setTracking(false)
+            shadeTestUtil.setLegacyExpandedOrAwaitingInputTransfer(true)
         } else {
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
-            shadeRepository.setLegacyShadeExpansion(0f)
-            shadeRepository.setLegacyShadeTracking(false)
-            shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(false)
+            shadeTestUtil.setShadeExpansion(0f)
+            shadeTestUtil.setTracking(false)
+            shadeTestUtil.setLegacyExpandedOrAwaitingInputTransfer(false)
         }
     }
 
-    private val testComponent: TestComponent =
-        DaggerDefaultUdfpsTouchOverlayViewModelTest_TestComponent.factory()
-            .create(
-                test = this,
-                featureFlags =
-                    FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) },
-                mocks = TestMocksModule(systemUIDialogManager = systemUIDialogManager),
-            )
-
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun shadeNotExpanded_noDialogShowing_shouldHandleTouchesTrue() =
-        testComponent.runTest {
+        testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
             runCurrent()
 
@@ -120,7 +118,7 @@
 
     @Test
     fun shadeNotExpanded_dialogShowing_shouldHandleTouchesFalse() =
-        testComponent.runTest {
+        testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
             runCurrent()
 
@@ -134,7 +132,7 @@
 
     @Test
     fun shadeExpanded_noDialogShowing_shouldHandleTouchesFalse() =
-        testComponent.runTest {
+        testScope.runTest {
             val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
             runCurrent()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index af1d315..f62a55d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -77,6 +77,8 @@
 
     @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
 
+    @Mock private lateinit var deviceItemActionInteractor: DeviceItemActionInteractor
+
     @Mock private lateinit var activityStarter: ActivityStarter
 
     @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
@@ -118,6 +120,7 @@
         bluetoothTileDialogViewModel =
             BluetoothTileDialogViewModel(
                 deviceItemInteractor,
+                deviceItemActionInteractor,
                 BluetoothStateInteractor(
                     localBluetoothManager,
                     bluetoothTileDialogLogger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt
new file mode 100644
index 0000000..762137b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt
@@ -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.systemui.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceItemActionInteractorImplTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+    private lateinit var actionInteractorImpl: DeviceItemActionInteractor
+
+    @Mock private lateinit var dialog: SystemUIDialog
+    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+    @Mock private lateinit var device: BluetoothDevice
+    @Mock private lateinit var deviceItem: DeviceItem
+
+    @Before
+    fun setUp() {
+        actionInteractorImpl = kosmos.deviceItemActionInteractor
+        whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedDevice)
+        whenever(cachedDevice.address).thenReturn("ADDRESS")
+        whenever(cachedDevice.device).thenReturn(device)
+    }
+
+    @Test
+    fun testOnClick_connectedMedia_setActive() {
+        with(kosmos) {
+            testScope.runTest {
+                whenever(deviceItem.type)
+                    .thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+                actionInteractorImpl.onClick(deviceItem, dialog)
+                verify(cachedDevice).setActive()
+                verify(bluetoothTileDialogLogger)
+                    .logDeviceClick(
+                        cachedDevice.address,
+                        DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
+                    )
+            }
+        }
+    }
+
+    @Test
+    fun testOnClick_activeMedia_disconnect() {
+        with(kosmos) {
+            testScope.runTest {
+                whenever(deviceItem.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+                actionInteractorImpl.onClick(deviceItem, dialog)
+                verify(cachedDevice).disconnect()
+                verify(bluetoothTileDialogLogger)
+                    .logDeviceClick(
+                        cachedDevice.address,
+                        DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE
+                    )
+            }
+        }
+    }
+
+    @Test
+    fun testOnClick_connectedOtherDevice_disconnect() {
+        with(kosmos) {
+            testScope.runTest {
+                whenever(deviceItem.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+                actionInteractorImpl.onClick(deviceItem, dialog)
+                verify(cachedDevice).disconnect()
+                verify(bluetoothTileDialogLogger)
+                    .logDeviceClick(cachedDevice.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            }
+        }
+    }
+
+    @Test
+    fun testOnClick_saved_connect() {
+        with(kosmos) {
+            testScope.runTest {
+                whenever(deviceItem.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+                actionInteractorImpl.onClick(deviceItem, dialog)
+                verify(cachedDevice).connect()
+                verify(bluetoothTileDialogLogger)
+                    .logDeviceClick(cachedDevice.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
new file mode 100644
index 0000000..e8e37bc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.internal.logging.uiEventLogger
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} }
+
+val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by
+    Kosmos.Fixture {
+        DeviceItemActionInteractorImpl(
+            testDispatcher,
+            bluetoothTileDialogLogger,
+            uiEventLogger,
+        )
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index 28cbcb4..4bcd9a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.bluetooth.qsdialog
 
 import android.bluetooth.BluetoothDevice
-import android.content.pm.PackageInfo
+import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.media.AudioManager
 import android.platform.test.annotations.DisableFlags
@@ -25,7 +25,6 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.flags.Flags
 import com.android.systemui.SysuiTestCase
@@ -120,11 +119,10 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
     fun testSavedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() {
-        val exclusiveManagerName =
-            BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
         `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
-            .thenReturn(exclusiveManagerName.toByteArray())
-        `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo())
+            .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+        `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+            .thenReturn(ApplicationInfo())
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(false)
 
@@ -144,11 +142,11 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
-    fun testSavedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() {
+    fun testSavedFactory_isFilterMatched_exclusiveManagerNotEnabled_returnsTrue() {
         `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
-            .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray())
-        `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0))
-            .thenReturn(PackageInfo())
+            .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+        `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+            .thenReturn(ApplicationInfo().also { it.enabled = false })
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(false)
 
@@ -158,12 +156,10 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
-    fun testSavedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() {
-        val exclusiveManagerName =
-            BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
+    fun testSavedFactory_isFilterMatched_exclusiveManagerNotInstalled_returnsTrue() {
         `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
-            .thenReturn(exclusiveManagerName.toByteArray())
-        `when`(packageManager.getPackageInfo(exclusiveManagerName, 0))
+            .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+        `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
             .thenThrow(PackageManager.NameNotFoundException("Test!"))
         `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(cachedDevice.isConnected).thenReturn(false)
@@ -228,11 +224,10 @@
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
     fun testConnectedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() {
-        val exclusiveManagerName =
-            BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
         `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
-            .thenReturn(exclusiveManagerName.toByteArray())
-        `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo())
+            .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+        `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+            .thenReturn(ApplicationInfo())
         `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(bluetoothDevice.isConnected).thenReturn(true)
         audioManager.setMode(AudioManager.MODE_NORMAL)
@@ -254,11 +249,11 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
-    fun testConnectedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() {
+    fun testConnectedFactory_isFilterMatched_exclusiveManagerNotEnabled_returnsTrue() {
         `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
-            .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray())
-        `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0))
-            .thenReturn(PackageInfo())
+            .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+        `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+            .thenReturn(ApplicationInfo().also { it.enabled = false })
         `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(bluetoothDevice.isConnected).thenReturn(true)
         audioManager.setMode(AudioManager.MODE_NORMAL)
@@ -269,12 +264,10 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
-    fun testConnectedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() {
-        val exclusiveManagerName =
-            BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
+    fun testConnectedFactory_isFilterMatched_exclusiveManagerNotInstalled_returnsTrue() {
         `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
-            .thenReturn(exclusiveManagerName.toByteArray())
-        `when`(packageManager.getPackageInfo(exclusiveManagerName, 0))
+            .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+        `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
             .thenThrow(PackageManager.NameNotFoundException("Test!"))
         `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
         `when`(bluetoothDevice.isConnected).thenReturn(true)
@@ -317,7 +310,7 @@
     companion object {
         const val DEVICE_NAME = "DeviceName"
         const val CONNECTION_SUMMARY = "ConnectionSummary"
-        private const val FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name"
+        private const val TEST_EXCLUSIVE_MANAGER = "com.test.manager"
         private const val DEVICE_ADDRESS = "04:52:C7:0B:D8:3C"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index daf4a3c..2b4f950 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -23,7 +23,6 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
-import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.systemui.SysuiTestCase
@@ -39,7 +38,6 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
@@ -71,8 +69,6 @@
 
     @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
 
-    @Mock private lateinit var uiEventLogger: UiEventLogger
-
     @Mock private lateinit var logger: BluetoothTileDialogLogger
 
     private val fakeSystemClock = FakeSystemClock()
@@ -94,7 +90,6 @@
                 adapter,
                 localBluetoothManager,
                 fakeSystemClock,
-                uiEventLogger,
                 logger,
                 testScope.backgroundScope,
                 dispatcher
@@ -218,61 +213,6 @@
         }
     }
 
-    @Test
-    fun testUpdateDeviceItemOnClick_connectedMedia_setActive() {
-        testScope.runTest {
-            `when`(deviceItem1.type).thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
-
-            interactor.updateDeviceItemOnClick(deviceItem1)
-
-            verify(cachedDevice1).setActive()
-            verify(logger)
-                .logDeviceClick(
-                    cachedDevice1.address,
-                    DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
-                )
-        }
-    }
-
-    @Test
-    fun testUpdateDeviceItemOnClick_activeMedia_disconnect() {
-        testScope.runTest {
-            `when`(deviceItem1.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
-
-            interactor.updateDeviceItemOnClick(deviceItem1)
-
-            verify(cachedDevice1).disconnect()
-            verify(logger)
-                .logDeviceClick(cachedDevice1.address, DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
-        }
-    }
-
-    @Test
-    fun testUpdateDeviceItemOnClick_connectedOtherDevice_disconnect() {
-        testScope.runTest {
-            `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-
-            interactor.updateDeviceItemOnClick(deviceItem1)
-
-            verify(cachedDevice1).disconnect()
-            verify(logger)
-                .logDeviceClick(cachedDevice1.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
-        }
-    }
-
-    @Test
-    fun testUpdateDeviceItemOnClick_saved_connect() {
-        testScope.runTest {
-            `when`(deviceItem1.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
-
-            interactor.updateDeviceItemOnClick(deviceItem1)
-
-            verify(cachedDevice1).connect()
-            verify(logger)
-                .logDeviceClick(cachedDevice1.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
-        }
-    }
-
     private fun createFactory(
         isFilterMatchFunc: (CachedBluetoothDevice) -> Boolean,
         deviceItem: DeviceItem
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
index bed05ee..cde7a0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
@@ -30,7 +30,6 @@
 import java.nio.charset.Charset
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -102,12 +101,6 @@
         assertThat(dataRead).isEqualTo(newDataToWrite)
     }
 
-    @Ignore("Ignored until we figure out why it is flaky b/336561027")
-    @Test(expected = FileNotFoundException::class)
-    fun read_fileNotFoundException() {
-        underTest.readBytesFromDisk()
-    }
-
     @Test(expected = FileNotFoundException::class)
     fun clear_returnsTrueWhenFileDeleted() {
         // Write bytes to disk
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
index 6a0462b..c39c3fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.deviceentry.domain.ui.viewmodel
 
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
@@ -24,7 +24,9 @@
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.deviceentry.data.ui.viewmodel.deviceEntryUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
 import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
 import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
@@ -34,19 +36,22 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 @ExperimentalCoroutinesApi
 @SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
@@ -59,8 +64,27 @@
     private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
     private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
     private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
-    private val shadeRepository = kosmos.fakeShadeRepository
-    private val underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel
+
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+    private lateinit var underTest: DeviceEntryUdfpsAccessibilityOverlayViewModel
+
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags)
+    }
+
+    @Before
+    fun setup() {
+        underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel
+    }
 
     @Test
     fun visible() =
@@ -142,7 +166,7 @@
         )
 
         // Shade not expanded
-        shadeRepository.qsExpansion.value = 0f
-        shadeRepository.lockscreenShadeExpansion.value = 0f
+        shadeTestUtil.setQsExpansion(0f)
+        shadeTestUtil.setLockscreenShadeExpansion(0f)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
new file mode 100644
index 0000000..05a2ca2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.ui
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
+import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShortcutHelperActivityStarterTest : SysuiTestCase() {
+
+    private val kosmos =
+        Kosmos().also {
+            it.testCase = this
+            it.testDispatcher = UnconfinedTestDispatcher()
+        }
+
+    private val testScope = kosmos.testScope
+    private val testHelper = kosmos.shortcutHelperTestHelper
+    private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
+    private val starter = kosmos.shortcutHelperActivityStarter
+
+    @Test
+    fun start_doesNotStartByDefault() =
+        testScope.runTest {
+            starter.start()
+
+            assertThat(fakeStartActivity.startIntents).isEmpty()
+        }
+
+    @Test
+    fun start_onToggle_startsActivity() =
+        testScope.runTest {
+            starter.start()
+
+            testHelper.toggle(deviceId = 456)
+
+            verifyShortcutHelperActivityStarted()
+        }
+
+    @Test
+    fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
+        testScope.runTest {
+            starter.start()
+
+            testHelper.toggle(deviceId = 456)
+            testHelper.toggle(deviceId = 456)
+            testHelper.toggle(deviceId = 456)
+            testHelper.toggle(deviceId = 456)
+
+            verifyShortcutHelperActivityStarted(numTimes = 2)
+        }
+
+    @Test
+    fun start_onRequestShowShortcuts_startsActivity() =
+        testScope.runTest {
+            starter.start()
+
+            testHelper.showFromActivity()
+
+            verifyShortcutHelperActivityStarted()
+        }
+
+    @Test
+    fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
+        testScope.runTest {
+            starter.start()
+
+            testHelper.showFromActivity()
+            testHelper.showFromActivity()
+            testHelper.showFromActivity()
+
+            verifyShortcutHelperActivityStarted(numTimes = 1)
+        }
+
+    @Test
+    fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyWhenNotStarted() =
+        testScope.runTest {
+            starter.start()
+
+            testHelper.hideFromActivity()
+            testHelper.hideForSystem()
+            testHelper.toggle(deviceId = 987)
+            testHelper.showFromActivity()
+            testHelper.hideFromActivity()
+            testHelper.hideForSystem()
+            testHelper.toggle(deviceId = 456)
+            testHelper.showFromActivity()
+
+            verifyShortcutHelperActivityStarted(numTimes = 2)
+        }
+
+    private fun verifyShortcutHelperActivityStarted(numTimes: Int = 1) {
+        assertThat(fakeStartActivity.startIntents).hasSize(numTimes)
+        fakeStartActivity.startIntents.forEach { intent ->
+            assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+            assertThat(intent.filterEquals(Intent(context, ShortcutHelperActivity::class.java)))
+                .isTrue()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
new file mode 100644
index 0000000..8653308
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.ui.viewmodel
+
+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.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShortcutHelperViewModelTest : SysuiTestCase() {
+
+    private val kosmos =
+        Kosmos().also {
+            it.testCase = this
+            it.testDispatcher = UnconfinedTestDispatcher()
+        }
+
+    private val testScope = kosmos.testScope
+    private val testHelper = kosmos.shortcutHelperTestHelper
+
+    private val viewModel = kosmos.shortcutHelperViewModel
+
+    @Test
+    fun shouldShow_falseByDefault() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(viewModel.shouldShow)
+
+            assertThat(shouldShow).isFalse()
+        }
+
+    @Test
+    fun shouldShow_trueAfterShowRequested() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(viewModel.shouldShow)
+
+            testHelper.showFromActivity()
+
+            assertThat(shouldShow).isTrue()
+        }
+
+    @Test
+    fun shouldShow_trueAfterToggleRequested() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(viewModel.shouldShow)
+
+            testHelper.toggle(deviceId = 123)
+
+            assertThat(shouldShow).isTrue()
+        }
+
+    @Test
+    fun shouldShow_falseAfterToggleTwice() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(viewModel.shouldShow)
+
+            testHelper.toggle(deviceId = 123)
+            testHelper.toggle(deviceId = 123)
+
+            assertThat(shouldShow).isFalse()
+        }
+
+    @Test
+    fun shouldShow_falseAfterViewDestroyed() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(viewModel.shouldShow)
+
+            testHelper.toggle(deviceId = 567)
+            viewModel.onUserLeave()
+
+            assertThat(shouldShow).isFalse()
+        }
+
+    @Test
+    fun shouldShow_doesNotEmitDuplicateValues() =
+        testScope.runTest {
+            val shouldShowValues by collectValues(viewModel.shouldShow)
+
+            testHelper.hideForSystem()
+            testHelper.toggle(deviceId = 987)
+            testHelper.showFromActivity()
+            viewModel.onUserLeave()
+            testHelper.hideFromActivity()
+            testHelper.hideForSystem()
+            testHelper.toggle(deviceId = 456)
+            testHelper.showFromActivity()
+
+            assertThat(shouldShowValues).containsExactly(false, true, false, true).inOrder()
+        }
+
+    @Test
+    fun shouldShow_emitsLatestValueToNewSubscribers() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(viewModel.shouldShow)
+
+            testHelper.showFromActivity()
+
+            val shouldShowNew by collectLastValue(viewModel.shouldShow)
+            assertThat(shouldShowNew).isEqualTo(shouldShow)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index bcaad01..f5b5261 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,24 +19,20 @@
 
 package com.android.systemui.keyguard.data.repository
 
-import android.os.fakeExecutorHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
 import com.android.systemui.util.ThreadAssert
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -50,31 +46,32 @@
 class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: KeyguardBlueprintRepository
     @Mock lateinit var configurationRepository: ConfigurationRepository
-    @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint
     @Mock lateinit var threadAssert: ThreadAssert
+
     private val testScope = TestScope(StandardTestDispatcher())
     private val kosmos: Kosmos = testKosmos()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        with(kosmos) {
-            whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
-            underTest =
-                KeyguardBlueprintRepository(
-                    setOf(defaultLockscreenBlueprint),
-                    fakeExecutorHandler,
-                    threadAssert,
-                )
-        }
+        underTest = kosmos.keyguardBlueprintRepository
     }
 
     @Test
     fun testApplyBlueprint_DefaultLayout() {
         testScope.runTest {
             val blueprint by collectLastValue(underTest.blueprint)
-            underTest.applyBlueprint(defaultLockscreenBlueprint)
-            assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+            underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)
+            assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
+        }
+    }
+
+    @Test
+    fun testApplyBlueprint_SplitShadeLayout() {
+        testScope.runTest {
+            val blueprint by collectLastValue(underTest.blueprint)
+            underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)
+            assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint)
         }
     }
 
@@ -83,33 +80,22 @@
         testScope.runTest {
             val blueprint by collectLastValue(underTest.blueprint)
             underTest.refreshBlueprint()
-            assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+            assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
         }
     }
 
     @Test
-    fun testTransitionToLayout_validId() {
-        assertThat(underTest.applyBlueprint(DEFAULT)).isTrue()
+    fun testTransitionToDefaultLayout_validId() {
+        assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue()
+    }
+
+    @Test
+    fun testTransitionToSplitShadeLayout_validId() {
+        assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue()
     }
 
     @Test
     fun testTransitionToLayout_invalidId() {
         assertThat(underTest.applyBlueprint("abc")).isFalse()
     }
-
-    @Test
-    fun testTransitionToSameBlueprint_refreshesBlueprint() =
-        with(kosmos) {
-            testScope.runTest {
-                val transition by collectLastValue(underTest.refreshTransition)
-                fakeExecutor.runAllReady()
-                runCurrent()
-
-                underTest.applyBlueprint(defaultLockscreenBlueprint)
-                fakeExecutor.runAllReady()
-                runCurrent()
-
-                assertThat(transition).isNotNull()
-            }
-        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index ac5823e..0bdf47a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.kosmos.testScope
@@ -40,6 +41,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -54,7 +56,7 @@
 class KeyguardBlueprintInteractorTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val underTest by lazy { kosmos.keyguardBlueprintInteractor }
+    private val underTest = kosmos.keyguardBlueprintInteractor
     private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository }
     private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
     private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository }
@@ -79,8 +81,9 @@
             val blueprintId by collectLastValue(underTest.blueprintId)
             kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
             configurationRepository.onConfigurationChange()
-            runCurrent()
 
+            runCurrent()
+            advanceUntilIdle()
             assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.Companion.DEFAULT)
         }
     }
@@ -92,8 +95,9 @@
             val blueprintId by collectLastValue(underTest.blueprintId)
             kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
             configurationRepository.onConfigurationChange()
-            runCurrent()
 
+            runCurrent()
+            advanceUntilIdle()
             assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID)
         }
     }
@@ -102,12 +106,13 @@
     @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
     fun fingerprintPropertyInitialized_updatesBlueprint() {
         testScope.runTest {
-            val blueprintId by collectLastValue(underTest.blueprintId)
-            kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
-            fingerprintPropertyRepository.supportsUdfps() // initialize properties
-            runCurrent()
+            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
 
-            assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID)
+            fingerprintPropertyRepository.supportsUdfps() // initialize properties
+
+            runCurrent()
+            advanceUntilIdle()
+            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
         }
     }
 
@@ -119,9 +124,23 @@
             kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
             clockRepository.setCurrentClock(clockController)
             configurationRepository.onConfigurationChange()
-            runCurrent()
 
+            runCurrent()
+            advanceUntilIdle()
             assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.DEFAULT)
         }
     }
+
+    @Test
+    fun testRefreshFromConfigChange() {
+        testScope.runTest {
+            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
+
+            configurationRepository.onConfigurationChange()
+
+            runCurrent()
+            advanceUntilIdle()
+            assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 1dc58d1..c3a806b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.app.StatusBarManager
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.keyguard.KeyguardSecurityModel
@@ -29,7 +31,9 @@
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.andSceneContainer
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeCommandQueue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -46,7 +50,8 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.commandQueue
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.whenever
@@ -61,13 +66,14 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
 import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 
 /**
  * Class for testing user journeys through the interactors. They will all be activated during setup,
@@ -75,8 +81,8 @@
  */
 @ExperimentalCoroutinesApi
 @SmallTest
-@RunWith(JUnit4::class)
-class KeyguardTransitionScenariosTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
@@ -87,7 +93,7 @@
     private val keyguardRepository = kosmos.fakeKeyguardRepository
     private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
     private var commandQueue = kosmos.fakeCommandQueue
-    private val shadeRepository = kosmos.fakeShadeRepository
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
     private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
     private lateinit var featureFlags: FakeFeatureFlags
 
@@ -112,6 +118,18 @@
     private val communalInteractor = kosmos.communalInteractor
     private val dockManager = kosmos.fakeDockManager
 
+    companion object {
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun getParams(): List<FlagsParameterization> {
+            return FlagsParameterization.allCombinationsOf().andSceneContainer()
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags!!)
+    }
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -119,9 +137,11 @@
         whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
 
         mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
-        mSetFlagsRule.disableFlags(
-            Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
-        )
+        if (!SceneContainerFlag.isEnabled) {
+            mSetFlagsRule.disableFlags(
+                Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
+            )
+        }
         featureFlags = FakeFeatureFlags()
 
         fromLockscreenTransitionInteractor.start()
@@ -210,6 +230,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun lockscreenToDreaming() =
         testScope.runTest {
             // GIVEN a device that is not dreaming or dozing
@@ -238,6 +259,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun lockscreenToDreamingLockscreenHosted() =
         testScope.runTest {
             // GIVEN a device that is not dreaming or dozing
@@ -527,6 +549,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun dozingToGoneWithUnlock() =
         testScope.runTest {
             // GIVEN a prior transition has run to DOZING
@@ -706,6 +729,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun goneToLockscreen() =
         testScope.runTest {
             // GIVEN a prior transition has run to GONE
@@ -755,6 +779,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun goneToGlanceableHub() =
         testScope.runTest {
             // GIVEN a prior transition has run to GONE
@@ -897,6 +922,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun alternateBouncerToGone() =
         testScope.runTest {
             // GIVEN a prior transition has run to ALTERNATE_BOUNCER
@@ -1135,6 +1161,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun occludedToGone() =
         testScope.runTest {
             // GIVEN a device on lockscreen
@@ -1165,6 +1192,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun occludedToLockscreen() =
         testScope.runTest {
             // GIVEN a device on lockscreen
@@ -1193,6 +1221,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun occludedToGlanceableHub() =
         testScope.runTest {
             // GIVEN a device on lockscreen
@@ -1229,6 +1258,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun occludedToGlanceableHubWhenInitiallyOnHub() =
         testScope.runTest {
             // GIVEN a device on lockscreen and communal is available
@@ -1314,6 +1344,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun primaryBouncerToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to PRIMARY_BOUNCER
@@ -1339,6 +1370,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun dozingToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to DOZING
@@ -1364,6 +1396,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun dreamingToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to DREAMING
@@ -1392,6 +1425,39 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
+    @EnableFlags(Flags.FLAG_RESTART_DREAM_ON_UNOCCLUDE)
+    fun dreamingToOccludedToDreaming() =
+        testScope.runTest {
+            // GIVEN a device on lockscreen
+            keyguardRepository.setKeyguardShowing(true)
+            runCurrent()
+
+            // Given a device that is dreaming
+            keyguardRepository.setDreaming(true)
+
+            // GIVEN a prior transition has run to OCCLUDED
+            runTransitionAndSetWakefulness(KeyguardState.DREAMING, KeyguardState.OCCLUDED)
+            keyguardRepository.setKeyguardOccluded(true)
+            runCurrent()
+
+            // WHEN occlusion ends
+            keyguardRepository.setKeyguardOccluded(false)
+            runCurrent()
+
+            // THEN a transition to GLANCEABLE_HUB should occur
+            assertThat(transitionRepository)
+                .startedTransition(
+                    ownerName = FromOccludedTransitionInteractor::class.simpleName,
+                    from = KeyguardState.OCCLUDED,
+                    to = KeyguardState.DREAMING,
+                    animatorAssertion = { it.isNotNull() },
+                )
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
     fun dreamingToPrimaryBouncer() =
         testScope.runTest {
             // GIVEN a prior transition has run to DREAMING
@@ -1484,6 +1550,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun lockscreenToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to LOCKSCREEN
@@ -1507,6 +1574,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun aodToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to AOD
@@ -1553,6 +1621,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun lockscreenToOccluded_fromCameraGesture() =
         testScope.runTest {
             // GIVEN a prior transition has run to LOCKSCREEN
@@ -1586,6 +1655,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun lockscreenToPrimaryBouncerDragging() =
         testScope.runTest {
             // GIVEN a prior transition has run to LOCKSCREEN
@@ -1595,8 +1665,8 @@
             // GIVEN the keyguard is showing locked
             keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
             runCurrent()
-            shadeRepository.setLegacyShadeTracking(true)
-            shadeRepository.setLegacyShadeExpansion(.9f)
+            shadeTestUtil.setTracking(true)
+            shadeTestUtil.setShadeExpansion(.9f)
             runCurrent()
 
             // THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
@@ -1613,8 +1683,8 @@
             // WHEN the user stops dragging and shade is back to expanded
             clearInvocations(transitionRepository)
             runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
-            shadeRepository.setLegacyShadeTracking(false)
-            shadeRepository.setLegacyShadeExpansion(1f)
+            shadeTestUtil.setTracking(false)
+            shadeTestUtil.setShadeExpansion(1f)
             runCurrent()
 
             // THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
@@ -1803,6 +1873,7 @@
         }
 
     @Test
+    @BrokenWithSceneContainer(339465026)
     fun glanceableHubToOccluded() =
         testScope.runTest {
             // GIVEN a prior transition has run to GLANCEABLE_HUB
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index dbf6a29..8a0613f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -66,25 +66,19 @@
     fun testHelp() {
         command().execute(pw, listOf("help"))
         verify(pw, atLeastOnce()).println(anyString())
-        verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+        verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
     }
 
     @Test
     fun testBlank() {
         command().execute(pw, listOf())
         verify(pw, atLeastOnce()).println(anyString())
-        verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+        verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
     }
 
     @Test
     fun testValidArg() {
         command().execute(pw, listOf("fake"))
-        verify(keyguardBlueprintInteractor).transitionToBlueprint("fake")
-    }
-
-    @Test
-    fun testValidArg_Int() {
-        command().execute(pw, listOf("1"))
-        verify(keyguardBlueprintInteractor).transitionToBlueprint(1)
+        verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake")
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
index 0bca367..f61ddeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
 import com.android.systemui.coroutines.collectLastValue
@@ -150,6 +151,51 @@
             assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.NONE)
         }
 
+    fun accessibilityDelegateHint_accessibilityNotEnabled() =
+        testScope.runTest {
+            val accessibilityDelegateHint by collectLastValue(underTest.accessibilityDelegateHint)
+            kosmos.fakeAccessibilityRepository.isEnabled.value = false
+            assertThat(accessibilityDelegateHint)
+                .isEqualTo(DeviceEntryIconView.AccessibilityHintType.NONE)
+        }
+
+    @Test
+    fun accessibilityDelegateHint_accessibilityEnabled_locked() =
+        testScope.runTest {
+            val accessibilityDelegateHint by collectLastValue(underTest.accessibilityDelegateHint)
+            kosmos.fakeAccessibilityRepository.isEnabled.value = true
+
+            // interactive lock icon
+            keyguardRepository.setKeyguardDismissible(false)
+            fingerprintPropertyRepository.supportsUdfps()
+
+            assertThat(accessibilityDelegateHint)
+                .isEqualTo(DeviceEntryIconView.AccessibilityHintType.AUTHENTICATE)
+
+            // non-interactive lock icon
+            keyguardRepository.setKeyguardDismissible(false)
+            fingerprintPropertyRepository.supportsRearFps()
+
+            assertThat(accessibilityDelegateHint)
+                .isEqualTo(DeviceEntryIconView.AccessibilityHintType.NONE)
+        }
+
+    @Test
+    fun accessibilityDelegateHint_accessibilityEnabled_unlocked() =
+        testScope.runTest {
+            val accessibilityDelegateHint by collectLastValue(underTest.accessibilityDelegateHint)
+            kosmos.fakeAccessibilityRepository.isEnabled.value = true
+
+            // interactive unlock icon
+            keyguardRepository.setKeyguardDismissible(true)
+            fingerprintPropertyRepository.supportsUdfps()
+            advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay
+            runCurrent()
+
+            assertThat(accessibilityDelegateHint)
+                .isEqualTo(DeviceEntryIconView.AccessibilityHintType.ENTER)
+        }
+
     private fun deviceEntryIconTransitionAlpha(alpha: Float) {
         deviceEntryIconTransition.setDeviceEntryParentViewAlpha(alpha)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 1881a9e..16421a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -88,7 +88,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     @Mock private lateinit var expandable: Expandable
     @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
@@ -115,7 +115,7 @@
     private val kosmos = testKosmos()
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 0c98cff..768d446 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -53,7 +53,7 @@
 
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardClockViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
     val underTest = kosmos.keyguardClockViewModel
@@ -67,7 +67,7 @@
     var faceConfig = ClockFaceConfig()
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
deleted file mode 100644
index 8cc3a85..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
+++ /dev/null
@@ -1,61 +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.qs.panels.domain.repository
-
-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.qs.panels.data.repository.IconTilesRepositoryImpl
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class IconTilesRepositoryImplTest : SysuiTestCase() {
-
-    private val underTest = IconTilesRepositoryImpl()
-
-    @Test
-    fun iconTilesSpecsIsValid() = runTest {
-        val tilesSpecs by collectLastValue(underTest.iconTilesSpecs)
-        assertThat(tilesSpecs).isEqualTo(ICON_ONLY_TILES_SPECS)
-    }
-
-    companion object {
-        private val ICON_ONLY_TILES_SPECS =
-            setOf(
-                TileSpec.create("airplane"),
-                TileSpec.create("battery"),
-                TileSpec.create("cameratoggle"),
-                TileSpec.create("cast"),
-                TileSpec.create("color_correction"),
-                TileSpec.create("inversion"),
-                TileSpec.create("saver"),
-                TileSpec.create("dnd"),
-                TileSpec.create("flashlight"),
-                TileSpec.create("location"),
-                TileSpec.create("mictoggle"),
-                TileSpec.create("nfc"),
-                TileSpec.create("night"),
-                TileSpec.create("rotation")
-            )
-    }
-}
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 b5ef8c2..db11c3e 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
@@ -500,6 +500,42 @@
         )
     }
 
+    @Test
+    fun onActivityLaunchAnimationEnd_onFreshTile_longPressPropertiesAreReset() {
+        // WHEN an activity launch animation ends on a fresh tile
+        tileView.onActivityLaunchAnimationEnd()
+
+        // THEN the tile's long-press effect properties are reset by default
+        assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
+    }
+
+    @Test
+    fun onUpdateLongPressEffectProperties_duringLongPressEffect_propertiesAreNotReset() {
+        // GIVEN a state that supports long-press
+        val state = QSTile.State()
+        tileView.changeState(state)
+
+        // WHEN the long-press effect is updating the properties
+        tileView.updateLongPressEffectProperties(1f)
+
+        // THEN the tile's long-press effect properties haven't reset
+        assertThat(tileView.haveLongPressPropertiesBeenReset).isFalse()
+    }
+
+    @Test
+    fun onActivityLaunchAnimationEnd_afterLongPressEffect_longPressPropertiesAreReset() {
+        // GIVEN a state that supports long-press and the long-press effect updating
+        val state = QSTile.State()
+        tileView.changeState(state)
+        tileView.updateLongPressEffectProperties(1f)
+
+        // WHEN an activity launch animation ends on a fresh tile
+        tileView.onActivityLaunchAnimationEnd()
+
+        // THEN the tile's long-press effect properties are reset
+        assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
+    }
+
     class FakeTileView(
         context: Context,
         collapsed: Boolean,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index 2536a93..9798562 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -5,6 +5,7 @@
 import static android.telephony.SignalStrength.NUM_SIGNAL_STRENGTH_BINS;
 import static android.telephony.SignalStrength.SIGNAL_STRENGTH_GREAT;
 import static android.telephony.SignalStrength.SIGNAL_STRENGTH_POOR;
+import static android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
@@ -217,6 +218,8 @@
         when(mAccessPointController.getMergedCarrierEntry()).thenReturn(mMergedCarrierEntry);
         when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
         when(SubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(SUB_ID);
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(info);
         when(mToastFactory.createToast(any(), anyString(), anyString(), anyInt(), anyInt()))
             .thenReturn(mSystemUIToast);
         when(mSystemUIToast.getView()).thenReturn(mToastView);
@@ -1083,19 +1086,34 @@
     }
 
     @Test
-    public void hasActiveSubId_activeSubIdListIsEmpty_returnFalse() {
-        when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{});
+    public void hasActiveSubIdOnDds_noDds_returnFalse() {
+        when(SubscriptionManager.getDefaultDataSubscriptionId())
+                .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
         mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
 
-        assertThat(mInternetDialogController.hasActiveSubId()).isFalse();
+        assertThat(mInternetDialogController.hasActiveSubIdOnDds()).isFalse();
     }
 
     @Test
-    public void hasActiveSubId_activeSubIdListNotEmpty_returnTrue() {
-        when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
+    public void hasActiveSubIdOnDds_activeDds_returnTrue() {
         mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
 
-        assertThat(mInternetDialogController.hasActiveSubId()).isTrue();
+        assertThat(mInternetDialogController.hasActiveSubIdOnDds()).isTrue();
+    }
+
+    @Test
+    public void hasActiveSubIdOnDds_activeDdsAndHasProvisioning_returnFalse() {
+        when(SubscriptionManager.getDefaultDataSubscriptionId())
+                .thenReturn(SUB_ID);
+        SubscriptionInfo info = mock(SubscriptionInfo.class);
+        when(info.isEmbedded()).thenReturn(true);
+        when(info.getProfileClass()).thenReturn(PROFILE_CLASS_PROVISIONING);
+        when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(info);
+
+        mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+        assertThat(mInternetDialogController.hasActiveSubIdOnDds()).isFalse();
     }
 
     private String getResourcesString(String name) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java
index 6f88891..aefcc87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java
@@ -251,7 +251,7 @@
         // Mobile network should be gone if the list of active subscriptionId is null.
         when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(false);
         when(mInternetDialogController.isAirplaneModeEnabled()).thenReturn(false);
-        when(mInternetDialogController.hasActiveSubId()).thenReturn(false);
+        when(mInternetDialogController.hasActiveSubIdOnDds()).thenReturn(false);
 
         mInternetDialogDelegate.updateDialog(true);
 
@@ -336,7 +336,7 @@
 
     @Test
     public void updateDialog_mobileDataIsEnabled_checkMobileDataSwitch() {
-        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
         when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
         when(mInternetDialogController.isMobileDataEnabled()).thenReturn(true);
         mMobileToggleSwitch.setChecked(false);
@@ -348,7 +348,7 @@
 
     @Test
     public void updateDialog_mobileDataIsNotChanged_checkMobileDataSwitch() {
-        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
         when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
         when(mInternetDialogController.isMobileDataEnabled()).thenReturn(false);
         mMobileToggleSwitch.setChecked(false);
@@ -361,7 +361,7 @@
     @Test
     public void updateDialog_wifiOnAndHasInternetWifi_showConnectedWifi() {
         mInternetDialogDelegate.dismissDialog();
-        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
         createInternetDialog();
         // The preconditions WiFi ON and Internet WiFi are already in setUp()
         doReturn(false).when(mInternetDialogController).activeNetworkIsCellular();
@@ -522,7 +522,7 @@
     public void updateDialog_showSecondaryDataSub() {
         mInternetDialogDelegate.dismissDialog();
         doReturn(1).when(mInternetDialogController).getActiveAutoSwitchNonDdsSubId();
-        doReturn(true).when(mInternetDialogController).hasActiveSubId();
+        doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
         doReturn(false).when(mInternetDialogController).isAirplaneModeEnabled();
         createInternetDialog();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 0f37143..bf7d909 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -69,8 +69,9 @@
 
     @Before
     fun setUp() {
-        whenever(controllerFactory.create(eq(0), any())).thenReturn(controller0)
-        whenever(controllerFactory.create(eq(1), any())).thenReturn(controller1)
+        whenever(controllerFactory.create(any(), any())).thenAnswer {
+            if (it.getArgument<Display>(0).displayId == 0) controller0 else controller1
+        }
         whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0)
         whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1)
     }
@@ -78,12 +79,14 @@
     @Test
     fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
         testScope.runTest {
-            setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+            val internalDisplay = display(TYPE_INTERNAL, id = 0)
+            val externalDisplay = display(TYPE_EXTERNAL, id = 1)
+            setDisplays(internalDisplay, externalDisplay)
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
-            verify(controllerFactory).create(eq(0), any())
-            verify(controllerFactory).create(eq(1), any())
+            verify(controllerFactory).create(eq(internalDisplay), any())
+            verify(controllerFactory).create(eq(externalDisplay), any())
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
@@ -107,7 +110,9 @@
     @Test
     fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
         testScope.runTest {
-            setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+            val internalDisplay = display(TYPE_INTERNAL, id = 0)
+            val externalDisplay = display(TYPE_EXTERNAL, id = 1)
+            setDisplays(internalDisplay, externalDisplay)
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(
                 createScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE),
@@ -115,8 +120,8 @@
                 callback
             )
 
-            verify(controllerFactory).create(eq(0), any())
-            verify(controllerFactory, never()).create(eq(1), any())
+            verify(controllerFactory).create(eq(internalDisplay), any())
+            verify(controllerFactory, never()).create(eq(externalDisplay), any())
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 3793970..5b47c94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -446,6 +446,7 @@
                 mUiEventLogger,
                 () -> mKosmos.getInteractionJankMonitor(),
                 mJavaAdapter,
+                () -> mKeyguardTransitionInteractor,
                 () -> mShadeInteractor,
                 () -> mKosmos.getDeviceUnlockedInteractor(),
                 () -> mKosmos.getSceneInteractor(),
@@ -600,6 +601,7 @@
                                 new UiEventLoggerFake(),
                                 () -> mKosmos.getInteractionJankMonitor(),
                                 mJavaAdapter,
+                                () -> mKeyguardTransitionInteractor,
                                 () -> mShadeInteractor,
                                 () -> mKosmos.getDeviceUnlockedInteractor(),
                                 () -> mKosmos.getSceneInteractor(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index a867b0f..45d0102 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -102,7 +102,7 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 @RunWithLooper(setAsMainLooper = true)
-class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
 
     @Mock private lateinit var view: NotificationShadeWindowView
     @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
@@ -160,7 +160,7 @@
     private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 347620a..83ad18b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -55,7 +55,7 @@
 @RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
 @EnableFlags(FooterViewRefactor.FLAG_NAME)
-class FooterViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos =
         testKosmos().apply {
             fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -79,7 +79,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt
new file mode 100644
index 0000000..5e50af3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Flags.FLAG_COMPACT_HEADS_UP_NOTIFICATION
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HeadsUpStyleProviderImplTest : SysuiTestCase() {
+
+    @Rule @JvmField val setFlagsRule = SetFlagsRule()
+
+    private lateinit var statusBarModeRepositoryStore: FakeStatusBarModeRepository
+    private lateinit var headsUpStyleProvider: HeadsUpStyleProviderImpl
+
+    @Before
+    fun setUp() {
+        statusBarModeRepositoryStore = FakeStatusBarModeRepository()
+        statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = true
+
+        headsUpStyleProvider = HeadsUpStyleProviderImpl(statusBarModeRepositoryStore)
+    }
+
+    @Test
+    @DisableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+    fun shouldApplyCompactStyle_returnsFalse_whenCompactFlagDisabled() {
+        assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isFalse()
+    }
+
+    @Test
+    @EnableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+    fun shouldApplyCompactStyle_returnsTrue_whenImmersiveModeEnabled() {
+        // GIVEN
+        statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = true
+
+        // THEN
+        assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+    fun shouldApplyCompactStyle_returnsFalse_whenImmersiveModeDisabled() {
+        // GIVEN
+        statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = false
+
+        // THEN
+        assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
index 4f0f91a..926c35f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
@@ -134,7 +134,8 @@
                 /* isHeadsUpAnimation= */ eq(true),
                 /* onStartedRunnable= */ any(),
                 /* onFinishedRunnable= */ runnableCaptor.capture(),
-                /* animationListener= */ any()
+                /* animationListener= */ any(),
+                /* clipSide= */ eq(ExpandableView.ClipSide.BOTTOM),
             )
 
         animatorTestRule.advanceTimeBy(disappearDuration) // move to the end of SSA animations
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
index 9b4f931..cb40f72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
@@ -66,7 +66,7 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 @TestableLooper.RunWithLooper
-class KeyguardBypassControllerTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardBypassControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val featureFlags = FakeFeatureFlags()
@@ -92,7 +92,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Captor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 127a3d7..269510e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -168,6 +168,8 @@
     private FakePowerRepository mPowerRepository;
     @Mock
     private UserTracker mUserTracker;
+    @Mock
+    private HeadsUpManager mHeadsUpManager;
     private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
     private ExpandableNotificationRow mNotificationRow;
     private ExpandableNotificationRow mBubbleNotificationRow;
@@ -222,13 +224,12 @@
                 mScreenOffAnimationController,
                 mStatusBarStateController).getPowerInteractor();
 
-        HeadsUpManager headsUpManager = mock(HeadsUpManager.class);
         NotificationLaunchAnimatorControllerProvider notificationAnimationProvider =
                 new NotificationLaunchAnimatorControllerProvider(
                         new NotificationLaunchAnimationInteractor(
                                 new NotificationLaunchAnimationRepository()),
                         mock(NotificationListContainer.class),
-                        headsUpManager,
+                        mHeadsUpManager,
                         mJankMonitor);
         mNotificationActivityStarter =
                 new StatusBarNotificationActivityStarter(
@@ -237,7 +238,7 @@
                         mHandler,
                         mUiBgExecutor,
                         mVisibilityProvider,
-                        headsUpManager,
+                        mHeadsUpManager,
                         mActivityStarter,
                         mCommandQueue,
                         mClickNotifier,
@@ -417,6 +418,51 @@
     }
 
     @Test
+    public void testOnNotificationBubbleIconClicked_unbubble_keyGuardShowing()
+            throws RemoteException {
+        NotificationEntry entry = mBubbleNotificationRow.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
+
+        // Given
+        sbn.getNotification().contentIntent = mContentIntent;
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+        // When
+        mNotificationActivityStarter.onNotificationBubbleIconClicked(entry);
+
+        // Then
+        verify(mBubblesManager).onUserChangedBubble(entry, false);
+
+        verify(mHeadsUpManager).removeNotification(entry.getKey(), true);
+
+        verifyNoMoreInteractions(mContentIntent);
+        verifyNoMoreInteractions(mShadeController);
+    }
+
+    @Test
+    public void testOnNotificationBubbleIconClicked_bubble_keyGuardShowing() {
+        NotificationEntry entry = mNotificationRow.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
+
+        // Given
+        sbn.getNotification().contentIntent = mContentIntent;
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+        // When
+        mNotificationActivityStarter.onNotificationBubbleIconClicked(entry);
+
+        // Then
+        verify(mBubblesManager).onUserChangedBubble(entry, true);
+
+        verify(mHeadsUpManager).removeNotification(entry.getKey(), true);
+
+        verify(mContentIntent, atLeastOnce()).isActivity();
+        verifyNoMoreInteractions(mContentIntent);
+    }
+
+    @Test
     public void testOnFullScreenIntentWhenDozing_wakeUpDevice() {
         // GIVEN entry that can has a full screen intent that can show
         PendingIntent fullScreenIntent = PendingIntent.getActivity(mContext, 1,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index b5525b1..36df61d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -295,6 +295,50 @@
         }
 
     @Test
+    fun subscriptions_subIsOnlyNtn_modelHasExclusivelyNtnTrue() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.subscriptions)
+
+            val onlyNtnSub =
+                mock<SubscriptionInfo>().also {
+                    whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(true)
+                    whenever(it.subscriptionId).thenReturn(45)
+                    whenever(it.groupUuid).thenReturn(GROUP_1)
+                    whenever(it.carrierName).thenReturn("NTN only")
+                    whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+                }
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(onlyNtnSub))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].isExclusivelyNonTerrestrial).isTrue()
+        }
+
+    @Test
+    fun subscriptions_subIsNotOnlyNtn_modelHasExclusivelyNtnFalse() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.subscriptions)
+
+            val notOnlyNtnSub =
+                mock<SubscriptionInfo>().also {
+                    whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(false)
+                    whenever(it.subscriptionId).thenReturn(45)
+                    whenever(it.groupUuid).thenReturn(GROUP_1)
+                    whenever(it.carrierName).thenReturn("NTN only")
+                    whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+                }
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(notOnlyNtnSub))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).hasSize(1)
+            assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse()
+        }
+
+    @Test
     fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
         testScope.runTest {
             val latest by collectLastValue(underTest.subscriptions)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 0b14be1..0f9cbfa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -42,14 +42,11 @@
 import com.google.common.truth.Truth.assertThat
 import java.util.UUID
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.yield
-import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -68,7 +65,7 @@
             set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
         }
 
-    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
 
     private val tableLogBuffer =
@@ -113,17 +110,12 @@
             )
     }
 
-    @After fun tearDown() {}
-
     @Test
     fun filteredSubscriptions_default() =
         testScope.runTest {
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
-
-            job.cancel()
         }
 
     // Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
@@ -133,12 +125,9 @@
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
-
-            job.cancel()
         }
 
     @Test
@@ -146,12 +135,9 @@
         testScope.runTest {
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
-            job.cancel()
         }
 
     @Test
@@ -160,12 +146,9 @@
             connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
-
-            job.cancel()
         }
 
     @Test
@@ -180,12 +163,9 @@
             connectionsRepository.setSubscriptions(listOf(sub1, sub2))
             connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf(sub1, sub2))
-
-            job.cancel()
         }
 
     @Test
@@ -202,13 +182,10 @@
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             // Filtered subscriptions should show the active one when the config is false
             assertThat(latest).isEqualTo(listOf(sub3))
-
-            job.cancel()
         }
 
     @Test
@@ -225,13 +202,10 @@
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             // Filtered subscriptions should show the active one when the config is false
             assertThat(latest).isEqualTo(listOf(sub4))
-
-            job.cancel()
         }
 
     @Test
@@ -248,14 +222,11 @@
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(true)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             // Filtered subscriptions should show the primary (non-opportunistic) if the config is
             // true
             assertThat(latest).isEqualTo(listOf(sub1))
-
-            job.cancel()
         }
 
     @Test
@@ -272,14 +243,11 @@
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(true)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             // Filtered subscriptions should show the primary (non-opportunistic) if the config is
             // true
             assertThat(latest).isEqualTo(listOf(sub1))
-
-            job.cancel()
         }
 
     @Test
@@ -297,12 +265,9 @@
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf(sub3))
-
-            job.cancel()
         }
 
     @Test
@@ -320,12 +285,9 @@
             whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
                 .thenReturn(false)
 
-            var latest: List<SubscriptionModel>? = null
-            val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.filteredSubscriptions)
 
             assertThat(latest).isEqualTo(listOf(sub1))
-
-            job.cancel()
         }
 
     @Test
@@ -446,313 +408,345 @@
         }
 
     @Test
+    fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() =
+        testScope.runTest {
+            val notExclusivelyNonTerrestrialSub =
+                SubscriptionModel(
+                    isExclusivelyNonTerrestrial = false,
+                    subscriptionId = 5,
+                    carrierName = "Carrier 5",
+                    profileClass = PROFILE_CLASS_UNSET,
+                )
+
+            connectionsRepository.setSubscriptions(listOf(notExclusivelyNonTerrestrialSub))
+
+            val latest by collectLastValue(underTest.filteredSubscriptions)
+
+            assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
+        }
+
+    @Test
+    fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() =
+        testScope.runTest {
+            val exclusivelyNonTerrestrialSub =
+                SubscriptionModel(
+                    isExclusivelyNonTerrestrial = true,
+                    subscriptionId = 5,
+                    carrierName = "Carrier 5",
+                    profileClass = PROFILE_CLASS_UNSET,
+                )
+
+            connectionsRepository.setSubscriptions(listOf(exclusivelyNonTerrestrialSub))
+
+            val latest by collectLastValue(underTest.filteredSubscriptions)
+
+            assertThat(latest).isEmpty()
+        }
+
+    @Test
+    fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() =
+        testScope.runTest {
+            val exclusivelyNonTerrestrialSub =
+                SubscriptionModel(
+                    isExclusivelyNonTerrestrial = true,
+                    subscriptionId = 5,
+                    carrierName = "Carrier 5",
+                    profileClass = PROFILE_CLASS_UNSET,
+                )
+            val otherSub1 =
+                SubscriptionModel(
+                    isExclusivelyNonTerrestrial = false,
+                    subscriptionId = 1,
+                    carrierName = "Carrier 1",
+                    profileClass = PROFILE_CLASS_UNSET,
+                )
+            val otherSub2 =
+                SubscriptionModel(
+                    isExclusivelyNonTerrestrial = false,
+                    subscriptionId = 2,
+                    carrierName = "Carrier 2",
+                    profileClass = PROFILE_CLASS_UNSET,
+                )
+
+            connectionsRepository.setSubscriptions(
+                listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+            )
+
+            val latest by collectLastValue(underTest.filteredSubscriptions)
+
+            assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
+        }
+
+    @Test
+    fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() =
+        testScope.runTest {
+            // Exclusively non-terrestrial sub
+            val exclusivelyNonTerrestrialSub =
+                SubscriptionModel(
+                    isExclusivelyNonTerrestrial = true,
+                    subscriptionId = 5,
+                    carrierName = "Carrier 5",
+                    profileClass = PROFILE_CLASS_UNSET,
+                )
+
+            // Opportunistic subs
+            val (sub3, sub4) =
+                createSubscriptionPair(
+                    subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+                    opportunistic = Pair(true, true),
+                    grouped = true,
+                )
+
+            // WHEN both an exclusively non-terrestrial sub and opportunistic sub pair is included
+            connectionsRepository.setSubscriptions(listOf(sub3, sub4, exclusivelyNonTerrestrialSub))
+            connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+
+            val latest by collectLastValue(underTest.filteredSubscriptions)
+
+            // THEN both the only-non-terrestrial sub and the non-active sub are filtered out,
+            // leaving only sub3.
+            assertThat(latest).isEqualTo(listOf(sub3))
+        }
+
+    @Test
     fun activeDataConnection_turnedOn() =
         testScope.runTest {
             CONNECTION_1.setDataEnabled(true)
-            var latest: Boolean? = null
-            val job =
-                underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+            val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun activeDataConnection_turnedOff() =
         testScope.runTest {
             CONNECTION_1.setDataEnabled(true)
-            var latest: Boolean? = null
-            val job =
-                underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
             CONNECTION_1.setDataEnabled(false)
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun activeDataConnection_invalidSubId() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job =
-                underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
 
             connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
-            yield()
 
             // An invalid active subId should tell us that data is off
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun failedConnection_default_validated_notFailed() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun failedConnection_notDefault_notValidated_notFailed() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = false
             connectionsRepository.defaultConnectionIsValidated.value = false
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun failedConnection_default_notValidated_failed() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = false
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun failedConnection_carrierMergedDefault_notValidated_failed() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.hasCarrierMergedConnection.value = true
             connectionsRepository.defaultConnectionIsValidated.value = false
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     /** Regression test for b/275076959. */
     @Test
     fun failedConnection_dataSwitchInSameGroup_notFailed() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
+            runCurrent()
 
             // WHEN there's a data change in the same subscription group
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
             connectionsRepository.defaultConnectionIsValidated.value = false
+            runCurrent()
 
             // THEN the default connection is *not* marked as failed because of forced validation
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun failedConnection_dataSwitchNotInSameGroup_isFailed() =
         testScope.runTest {
-            var latestConnectionFailed: Boolean? = null
-            val job =
-                underTest.isDefaultConnectionFailed
-                    .onEach { latestConnectionFailed = it }
-                    .launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
+            runCurrent()
 
             // WHEN the connection is invalidated without a activeSubChangedInGroupEvent
             connectionsRepository.defaultConnectionIsValidated.value = false
 
             // THEN the connection is immediately marked as failed
-            assertThat(latestConnectionFailed).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun alwaysShowDataRatIcon_configHasTrue() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
 
             val config = MobileMappings.Config()
             config.alwaysShowDataRatIcon = true
             connectionsRepository.defaultDataSubRatConfig.value = config
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun alwaysShowDataRatIcon_configHasFalse() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
 
             val config = MobileMappings.Config()
             config.alwaysShowDataRatIcon = false
             connectionsRepository.defaultDataSubRatConfig.value = config
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun alwaysUseCdmaLevel_configHasTrue() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
 
             val config = MobileMappings.Config()
             config.alwaysShowCdmaRssi = true
             connectionsRepository.defaultDataSubRatConfig.value = config
-            yield()
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun alwaysUseCdmaLevel_configHasFalse() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
 
             val config = MobileMappings.Config()
             config.alwaysShowCdmaRssi = false
             connectionsRepository.defaultDataSubRatConfig.value = config
-            yield()
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun isSingleCarrier_zeroSubscriptions_false() =
         testScope.runTest {
-            var latest: Boolean? = true
-            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(emptyList())
-            assertThat(latest).isFalse()
 
-            job.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun isSingleCarrier_oneSubscription_true() =
         testScope.runTest {
-            var latest: Boolean? = false
-            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1))
-            assertThat(latest).isTrue()
 
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isSingleCarrier_twoSubscriptions_false() =
         testScope.runTest {
-            var latest: Boolean? = true
-            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
-            assertThat(latest).isFalse()
 
-            job.cancel()
+            assertThat(latest).isFalse()
         }
 
     @Test
     fun isSingleCarrier_updates() =
         testScope.runTest {
-            var latest: Boolean? = false
-            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isSingleCarrier)
 
             connectionsRepository.setSubscriptions(listOf(SUB_1))
             assertThat(latest).isTrue()
 
             connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = false
             connectionsRepository.hasCarrierMergedConnection.value = false
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
     fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.hasCarrierMergedConnection.value = false
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     /** Regression test for b/272586234. */
     @Test
     fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = false
             connectionsRepository.hasCarrierMergedConnection.value = true
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun mobileIsDefault_updatesWhenRepoUpdates() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.mobileIsDefault)
 
             connectionsRepository.mobileIsDefault.value = true
             assertThat(latest).isTrue()
@@ -762,8 +756,6 @@
 
             connectionsRepository.hasCarrierMergedConnection.value = true
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     // The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow
@@ -772,95 +764,79 @@
     @Test
     fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() =
         testScope.runTest {
-            var latestConnectionFailed: Boolean? = null
-            val job =
-                underTest.isDefaultConnectionFailed
-                    .onEach { latestConnectionFailed = it }
-                    .launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
+            runCurrent()
 
             // Trigger a data change in the same subscription group that's not yet validated
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
             connectionsRepository.defaultConnectionIsValidated.value = false
+            runCurrent()
 
             // After 1s, the force validation bit is still present, so the connection is not marked
             // as failed
             advanceTimeBy(1000)
-            assertThat(latestConnectionFailed).isFalse()
+            assertThat(latest).isFalse()
 
             // After 2s, the force validation expires so the connection updates to failed
             advanceTimeBy(1001)
-            assertThat(latestConnectionFailed).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() =
         testScope.runTest {
-            var latestConnectionFailed: Boolean? = null
-            val job =
-                underTest.isDefaultConnectionFailed
-                    .onEach { latestConnectionFailed = it }
-                    .launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = false
+            runCurrent()
 
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
 
-            assertThat(latestConnectionFailed).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() =
         testScope.runTest {
-            var latestConnectionFailed: Boolean? = null
-            val job =
-                underTest.isDefaultConnectionFailed
-                    .onEach { latestConnectionFailed = it }
-                    .launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
 
             // GIVEN the network starts validated
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
+            runCurrent()
 
             // WHEN a data change happens in the same group
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
 
             // WHEN the validation bit is lost
             connectionsRepository.defaultConnectionIsValidated.value = false
+            runCurrent()
 
             // WHEN another data change happens in the same group
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
 
             // THEN the forced validation bit is still used...
-            assertThat(latestConnectionFailed).isFalse()
+            assertThat(latest).isFalse()
 
             advanceTimeBy(1000)
-            assertThat(latestConnectionFailed).isFalse()
+            assertThat(latest).isFalse()
 
             // ... but expires after 2s
             advanceTimeBy(1001)
-            assertThat(latestConnectionFailed).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun dataSwitch_whileAlreadyForcingValidation_resetsClock() =
         testScope.runTest {
-            var latestConnectionFailed: Boolean? = null
-            val job =
-                underTest.isDefaultConnectionFailed
-                    .onEach { latestConnectionFailed = it }
-                    .launchIn(this)
+            val latest by collectLastValue(underTest.isDefaultConnectionFailed)
             connectionsRepository.mobileIsDefault.value = true
             connectionsRepository.defaultConnectionIsValidated.value = true
+            runCurrent()
 
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
 
@@ -869,44 +845,37 @@
             // WHEN another change in same group event happens
             connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
             connectionsRepository.defaultConnectionIsValidated.value = false
+            runCurrent()
 
             // THEN the forced validation remains for exactly 2 more seconds from now
 
             // 1.500s from second event
             advanceTimeBy(1500)
-            assertThat(latestConnectionFailed).isFalse()
+            assertThat(latest).isFalse()
 
             // 2.001s from the second event
             advanceTimeBy(501)
-            assertThat(latestConnectionFailed).isTrue()
-
-            job.cancel()
+            assertThat(latest).isTrue()
         }
 
     @Test
     fun isForceHidden_repoHasMobileHidden_true() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isForceHidden)
 
             connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
 
             assertThat(latest).isTrue()
-
-            job.cancel()
         }
 
     @Test
     fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
         testScope.runTest {
-            var latest: Boolean? = null
-            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+            val latest by collectLastValue(underTest.isForceHidden)
 
             connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
 
             assertThat(latest).isFalse()
-
-            job.cancel()
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cfa734a1..ab10bc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -47,7 +47,7 @@
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardStatusBarViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
@@ -66,7 +66,7 @@
     }
 
     init {
-        mSetFlagsRule.setFlagsParameterization(flags!!)
+        mSetFlagsRule.setFlagsParameterization(flags)
     }
 
     @Before
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
index 4085b1b..923b636 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
@@ -25,8 +25,9 @@
 @SysUISingleton
 class FakeAccessibilityRepository(
     override val isTouchExplorationEnabled: MutableStateFlow<Boolean>,
+    override val isEnabled: MutableStateFlow<Boolean>,
 ) : AccessibilityRepository {
-    @Inject constructor() : this(MutableStateFlow(false))
+    @Inject constructor() : this(MutableStateFlow(false), MutableStateFlow(false))
 }
 
 @Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
index 66c9afb..b23767e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
@@ -17,5 +17,13 @@
 package com.android.systemui.animation
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
 
-val Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator() }
+val Kosmos.activityTransitionAnimator by
+    Kosmos.Fixture {
+        ActivityTransitionAnimator(
+            // The main thread is checked in a bunch of places inside the different transitions
+            // animators, so we have to pass the real main executor here.
+            mainExecutor = testCase.context.mainExecutor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
index 77cb167..5a092f3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
@@ -19,7 +19,13 @@
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testCase
 
 val Kosmos.dialogTransitionAnimator by Fixture {
-    fakeDialogTransitionAnimator(interactionJankMonitor = interactionJankMonitor)
+    fakeDialogTransitionAnimator(
+        // The main thread is checked in a bunch of places inside the different transitions
+        // animators, so we have to pass the real main executor here.
+        mainExecutor = testCase.context.mainExecutor,
+        interactionJankMonitor = interactionJankMonitor,
+    )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
index 48b72d0..1709329 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
@@ -15,17 +15,20 @@
 package com.android.systemui.animation
 
 import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
 
 /** A [DialogTransitionAnimator] to be used in tests. */
 @JvmOverloads
 fun fakeDialogTransitionAnimator(
+    @Main mainExecutor: Executor,
     isUnlocked: Boolean = true,
     isShowingAlternateAuthOnUnlock: Boolean = false,
     isPredictiveBackQsDialogAnim: Boolean = false,
     interactionJankMonitor: InteractionJankMonitor,
 ): DialogTransitionAnimator {
     return DialogTransitionAnimator(
+        mainExecutor = mainExecutor,
         callback =
             FakeCallback(
                 isUnlocked = isUnlocked,
@@ -36,7 +39,7 @@
             object : AnimationFeatureFlags {
                 override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
             },
-        transitionAnimator = fakeTransitionAnimator(),
+        transitionAnimator = fakeTransitionAnimator(mainExecutor),
         isForTesting = true,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
index bc7ec3f..d07875f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
@@ -15,10 +15,12 @@
 package com.android.systemui.animation
 
 import com.android.app.animation.Interpolators
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
 
 /** A [TransitionAnimator] to be used in tests. */
-fun fakeTransitionAnimator(): TransitionAnimator {
-    return TransitionAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
+fun fakeTransitionAnimator(@Main mainExecutor: Executor): TransitionAnimator {
+    return TransitionAnimator(mainExecutor, TEST_TIMINGS, TEST_INTERPOLATORS)
 }
 
 /**
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
index 3a61bf6..9b3482b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
@@ -18,8 +18,12 @@
 
 import android.os.UserHandle
 import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
 import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filter
 
 class FakePackageChangeRepository(private val systemClock: SystemClock) : PackageChangeRepository {
@@ -31,6 +35,15 @@
             user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid)
         }
 
+    private val _packageInstallSessions = MutableStateFlow<List<PackageInstallSession>>(emptyList())
+
+    override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+        _packageInstallSessions.asStateFlow()
+
+    fun setInstallSessions(sessions: List<PackageInstallSession>) {
+        _packageInstallSessions.value = sessions
+    }
+
     suspend fun notifyChange(model: PackageChangeModel) {
         _packageChanged.emit(model)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 329c0f1..f7ce367 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -51,6 +51,7 @@
     override fun abortRestoreWidgets() {}
 
     private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
-        _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
+        _communalWidgets.value +=
+            listOf(CommunalWidgetContentModel.Available(id, providerInfo, priority))
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
new file mode 100644
index 0000000..3190171
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
@@ -0,0 +1,28 @@
+/*
+ * 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
+
+import android.content.Intent
+
+class FakeShortcutHelperStartActivity : (Intent) -> Unit {
+
+    val startIntents = mutableListOf<Intent>()
+
+    override fun invoke(intent: Intent) {
+        startIntents += intent
+    }
+}
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
new file mode 100644
index 0000000..38f2a56
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -0,0 +1,60 @@
+/*
+ * 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
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.shortcutHelperRepository by
+    Kosmos.Fixture { ShortcutHelperRepository(fakeCommandQueue, broadcastDispatcher) }
+
+val Kosmos.shortcutHelperTestHelper by
+    Kosmos.Fixture {
+        ShortcutHelperTestHelper(
+            shortcutHelperRepository,
+            applicationContext,
+            broadcastDispatcher,
+            fakeCommandQueue
+        )
+    }
+
+val Kosmos.shortcutHelperInteractor by
+    Kosmos.Fixture { ShortcutHelperInteractor(shortcutHelperRepository) }
+
+val Kosmos.shortcutHelperViewModel by
+    Kosmos.Fixture { ShortcutHelperViewModel(testDispatcher, shortcutHelperInteractor) }
+
+val Kosmos.fakeShortcutHelperStartActivity by Kosmos.Fixture { FakeShortcutHelperStartActivity() }
+
+val Kosmos.shortcutHelperActivityStarter by
+    Kosmos.Fixture {
+        ShortcutHelperActivityStarter(
+            applicationContext,
+            applicationCoroutineScope,
+            shortcutHelperViewModel,
+            fakeShortcutHelperStartActivity,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
new file mode 100644
index 0000000..772ce41
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.content.Context
+import android.content.Intent
+import com.android.systemui.broadcast.FakeBroadcastDispatcher
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+
+class ShortcutHelperTestHelper(
+    repo: ShortcutHelperRepository,
+    private val context: Context,
+    private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
+    private val fakeCommandQueue: FakeCommandQueue,
+) {
+
+    init {
+        repo.start()
+    }
+
+    fun hideFromActivity() {
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS)
+        )
+    }
+
+    fun showFromActivity() {
+        fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS)
+        )
+    }
+
+    fun toggle(deviceId: Int) {
+        fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) }
+    }
+
+    fun hideForSystem() {
+        fakeCommandQueue.doForEachCallback { it.dismissKeyboardShortcutsMenu() }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 58b0ff8..67fa857 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntrySourceInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
@@ -49,6 +50,7 @@
         keyguardViewController = { statusBarKeyguardViewManager },
         deviceEntryInteractor = deviceEntryInteractor,
         deviceEntrySourceInteractor = deviceEntrySourceInteractor,
+        accessibilityInteractor = accessibilityInteractor,
         scope = testScope.backgroundScope,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index 3762497..ec56327 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -33,6 +34,7 @@
             uiEventLogger,
             { interactionJankMonitor },
             mock(),
+            { keyguardTransitionInteractor },
             { shadeInteractor },
             { deviceUnlockedInteractor },
             { sceneInteractor },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
similarity index 97%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
index 302ac35..093ebd6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs.pipeline.domain.interactor
+package com.android.systemui.qs
 
 import com.android.internal.logging.InstanceId
 import com.android.systemui.animation.Expandable
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt
new file mode 100644
index 0000000..d686699
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.packageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
+import com.android.systemui.settings.userTracker
+import com.android.systemui.util.mockito.whenever
+
+val Kosmos.iconAndNameCustomRepository by
+    Kosmos.Fixture {
+        whenever(userTracker.userContext.packageManager).thenReturn(packageManager)
+        IconAndNameCustomRepository(
+            installedTilesRepository,
+            userTracker,
+            backgroundCoroutineContext,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt
new file mode 100644
index 0000000..ff33650
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+
+var Kosmos.stockTilesRepository by
+    Kosmos.Fixture {
+        testCase.context.orCreateTestableResources
+        StockTilesRepository(testCase.context.resources)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt
new file mode 100644
index 0000000..bd54fd4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+
+val Kosmos.editTilesListInteractor by
+    Kosmos.Fixture {
+        EditTilesListInteractor(
+            stockTilesRepository,
+            qSTileConfigProvider,
+            iconAndNameCustomRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
new file mode 100644
index 0000000..612a5d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor
+
+val Kosmos.editModeViewModel by
+    Kosmos.Fixture {
+        EditModeViewModel(
+            editTilesListInteractor,
+            currentTilesInteractor,
+            minimumTilesInteractor,
+            infiniteGridLayout,
+            applicationCoroutineScope,
+            gridLayoutTypeInteractor,
+            gridLayoutMap,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
index ff6b7d0..ed4c67e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
@@ -17,23 +17,78 @@
 package com.android.systemui.qs.pipeline.data.repository
 
 import android.content.ComponentName
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
 
 class FakeInstalledTilesComponentRepository : InstalledTilesComponentRepository {
 
-    private val installedComponentsPerUser =
-        mutableMapOf<Int, MutableStateFlow<Set<ComponentName>>>()
+    private val installedServicesPerUser = mutableMapOf<Int, MutableStateFlow<List<ServiceInfo>>>()
 
     override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
-        return getFlow(userId).asStateFlow()
+        return getFlow(userId).map { it.map { it.componentName }.toSet() }
+    }
+
+    override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> {
+        return getFlow(userId).value
     }
 
     fun setInstalledPackagesForUser(userId: Int, components: Set<ComponentName>) {
-        getFlow(userId).value = components
+        getFlow(userId).value =
+            components.map {
+                ServiceInfo().apply {
+                    packageName = it.packageName
+                    name = it.className
+                    applicationInfo = ApplicationInfo()
+                }
+            }
     }
 
-    private fun getFlow(userId: Int): MutableStateFlow<Set<ComponentName>> =
-        installedComponentsPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
+    fun setInstalledServicesForUser(userId: Int, services: List<ServiceInfo>) {
+        getFlow(userId).value = services.toList()
+    }
+
+    private fun getFlow(userId: Int): MutableStateFlow<List<ServiceInfo>> =
+        installedServicesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) }
+
+    companion object {
+        fun ServiceInfo(
+            componentName: ComponentName,
+            serviceName: String,
+            serviceIcon: Drawable? = null,
+            appName: String = "",
+            appIcon: Drawable? = null
+        ): ServiceInfo {
+            val appInfo =
+                object : ApplicationInfo() {
+                    override fun loadLabel(pm: PackageManager): CharSequence {
+                        return appName
+                    }
+
+                    override fun loadIcon(pm: PackageManager?): Drawable? {
+                        return appIcon
+                    }
+                }
+            val serviceInfo =
+                object : ServiceInfo() {
+                        override fun loadLabel(pm: PackageManager): CharSequence {
+                            return serviceName
+                        }
+
+                        override fun loadIcon(pm: PackageManager?): Drawable? {
+                            return serviceIcon ?: getApplicationInfo().loadIcon(pm)
+                        }
+                    }
+                    .apply {
+                        packageName = componentName.packageName
+                        name = componentName.className
+                        applicationInfo = appInfo
+                    }
+            return serviceInfo
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt
new file mode 100644
index 0000000..ef1189f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.pipeline.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository
+
+var Kosmos.minimumTilesInteractor by
+    Kosmos.Fixture { MinimumTilesInteractor(minimumTilesRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
new file mode 100644
index 0000000..5c21ab6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night
+
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsNightDisplayTileConfig by
+    Kosmos.Fixture { QSAccessibilityModule.provideNightDisplayTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index 38ede44..ea02d0c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -76,6 +76,16 @@
         delegate.assertFlagValid()
         delegate.programmaticCollapseShade()
     }
+
+    fun setQsFullscreen(qsFullscreen: Boolean) {
+        delegate.assertFlagValid()
+        delegate.setQsFullscreen(qsFullscreen)
+    }
+
+    fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean) {
+        delegate.assertFlagValid()
+        delegate.setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer)
+    }
 }
 
 /** Sets up shade state for tests for a specific value of the scene container flag. */
@@ -103,6 +113,10 @@
 
     /** Sets the shade to half collapsed with no touch input. */
     fun programmaticCollapseShade()
+
+    fun setQsFullscreen(qsFullscreen: Boolean)
+
+    fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean)
 }
 
 /** Sets up shade state for tests when the scene container flag is disabled. */
@@ -146,6 +160,14 @@
         shadeRepository.setLegacyShadeExpansion(.5f)
         testScope.runCurrent()
     }
+
+    override fun setQsFullscreen(qsFullscreen: Boolean) {
+        shadeRepository.legacyQsFullscreen.value = true
+    }
+
+    override fun setLegacyExpandedOrAwaitingInputTransfer(expanded: Boolean) {
+        shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(expanded)
+    }
 }
 
 /** Sets up shade state for tests when the scene container flag is enabled. */
@@ -183,6 +205,16 @@
         setTransitionProgress(Scenes.Shade, Scenes.Lockscreen, .5f, false)
     }
 
+    override fun setQsFullscreen(qsFullscreen: Boolean) {
+        setQsExpansion(1f)
+    }
+
+    override fun setLegacyExpandedOrAwaitingInputTransfer(
+        legacyExpandedOrAwaitingInputTransfer: Boolean
+    ) {
+        setShadeExpansion(.1f)
+    }
+
     override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
         if (lockscreenShadeExpansion == 0f) {
             setIdleScene(Scenes.Lockscreen)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 3e9ae4d..1f2ecb7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -37,7 +37,7 @@
 class FakeUserRepository @Inject constructor() : UserRepository {
     companion object {
         // User id to represent a non system (human) user id. We presume this is the main user.
-        private const val MAIN_USER_ID = 10
+        const val MAIN_USER_ID = 10
 
         private const val DEFAULT_SELECTED_USER = 0
         private val DEFAULT_SELECTED_USER_INFO =
@@ -84,6 +84,10 @@
 
     override var isRefreshUsersPaused: Boolean = false
 
+    override suspend fun getMainUserId(): Int? {
+        return MAIN_USER_ID
+    }
+
     var refreshUsersCallCount: Int = 0
         private set
 
diff --git a/ravenwood/scripts/convert-androidtest.py b/ravenwood/scripts/convert-androidtest.py
new file mode 100755
index 0000000..61ec54b
--- /dev/null
+++ b/ravenwood/scripts/convert-androidtest.py
@@ -0,0 +1,184 @@
+#!/usr/bin/python3
+# 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.
+
+# This script converts a legacy test class (using AndroidTestCase, TestCase or
+# InstrumentationTestCase to a modern style test class, in a best-effort manner.
+#
+# Usage:
+#  convert-androidtest.py TARGET-FILE [TARGET-FILE ...]
+#
+# Caveats:
+#   - It adds all the extra imports, even if they're not needed.
+#   - It won't sort imports.
+#   - It also always adds getContext() and getTestContext().
+#
+
+import sys
+import fileinput
+import re
+import subprocess
+
+# Print message on console
+def log(msg):
+    print(msg, file=sys.stderr)
+
+
+# Matches `extends AndroidTestCase` (or another similar base class)
+re_extends = re.compile(
+    r''' \b extends \s+ (AndroidTestCase|TestCase|InstrumentationTestCase) \s* ''',
+    re.S + re.X)
+
+
+# Look into given files and return the files that have `re_extends`.
+def find_target_files(files):
+    ret = []
+
+    for file in files:
+        try:
+            with open(file, 'r') as f:
+                data = f.read()
+
+                if re_extends.search(data):
+                    ret.append(file)
+
+        except FileNotFoundError as e:
+            log(f'Failed to open file {file}: {e}')
+
+    return ret
+
+
+def main(args):
+    files = args
+
+    # Find the files that should be processed.
+    files = find_target_files(files)
+
+    if len(files) == 0:
+        log("No target files found.")
+        return 0
+
+    # Process the files.
+    with fileinput.input(files=(files), inplace = True, backup = '.bak') as f:
+        import_seen = False
+        carry_over = ''
+        class_body_started = False
+        class_seen = False
+
+        def on_file_start():
+            nonlocal import_seen, carry_over, class_body_started, class_seen
+            import_seen = False
+            carry_over = ''
+            class_body_started = False
+            class_seen = False
+
+        for line in f:
+            if (fileinput.filelineno() == 1):
+                log(f"Processing: {fileinput.filename()}")
+                on_file_start()
+
+            line = line.rstrip('\n')
+
+            # Carry over a certain line to the next line.
+            if re.search(r'''@Override\b''', line):
+                carry_over = carry_over + line + '\n'
+                continue
+
+            if carry_over:
+                line = carry_over + line
+                carry_over = ''
+
+
+            # Remove the base class from the class definition.
+            line = re_extends.sub('', line)
+
+            # Add a @RunWith.
+            if not class_seen and re.search(r'''\b class \b''', line, re.X):
+                class_seen = True
+                print("@RunWith(AndroidJUnit4.class)")
+
+
+            # Inject extra imports.
+            if not import_seen and re.search(r'''^import\b''', line):
+                import_seen = True
+                print("""\
+import android.content.Context;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertSame;
+import static junit.framework.TestCase.assertNotSame;
+import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNull;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+""")
+
+            # Add @Test to the test methods.
+            if re.search(r'''^ \s* public \s* void \s* test''', line, re.X):
+                print("    @Test")
+
+            # Convert setUp/tearDown to @Before/@After.
+            if re.search(r''' ^\s+ ( \@Override \s+ ) ? (public|protected) \s+ void \s+ (setUp|tearDown) ''',
+                        line, re.X):
+                if re.search('setUp', line):
+                    print('    @Before')
+                else:
+                    print('    @After')
+
+                line = re.sub(r''' \s* \@Override \s* \n ''', '', line, 0, re.X)
+                line = re.sub(r'''protected''', 'public', line, 0, re.X)
+
+            # Remove the super setUp / tearDown call.
+            if re.search(r''' \b super \. (setUp|tearDown) \b ''', line, re.X):
+                continue
+
+            # Convert mContext to getContext().
+            line = re.sub(r'''\b mContext \b ''', 'getContext()', line, 0, re.X)
+
+            # Print the processed line.
+            print(line)
+
+            # Add getContext() / getTestContext() at the beginning of the class.
+            if not class_body_started and re.search(r'''\{''', line):
+                class_body_started = True
+                print("""\
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    private Context getTestContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+""")
+
+
+    # Run diff
+    for file in files:
+        subprocess.call(["diff", "-u", "--color=auto", f"{file}.bak", file])
+
+    log(f'{len(files)} file(s) converted.')
+
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 2f54f8c..2a7458f 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -1469,10 +1469,7 @@
             int policyFlags = mState.getLastReceivedPolicyFlags();
             if (mState.isDragging()) {
                 // Send an event to the end of the drag gesture.
-                int pointerIdBits = ALL_POINTER_ID_BITS;
-                if (Flags.fixDragPointerWhenEndingDrag()) {
-                    pointerIdBits = 1 << mDraggingPointerId;
-                }
+                int pointerIdBits = 1 << mDraggingPointerId;
                 mDispatcher.sendMotionEvent(event, ACTION_UP, rawEvent, pointerIdBits, policyFlags);
             }
             mState.startDelegating();
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index e830523..249b3cb 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -83,6 +83,7 @@
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
@@ -172,6 +173,7 @@
     private static final String TAG = "AppWidgetServiceImpl";
 
     private static final boolean DEBUG = false;
+    private static final boolean DEBUG_NULL_PROVIDER_INFO = Build.IS_DEBUGGABLE;
 
     private static final String OLD_KEYGUARD_HOST_PACKAGE = "android";
     private static final String NEW_KEYGUARD_HOST_PACKAGE = "com.android.keyguard";
@@ -736,7 +738,10 @@
         }
         RemoteViews views = new RemoteViews(mContext.getPackageName(),
                 R.layout.work_widget_mask_view);
-        ApplicationInfo appInfo = provider.info.providerInfo.applicationInfo;
+        final ActivityInfo activityInfo = provider.info.providerInfo;
+        final ApplicationInfo appInfo = activityInfo != null ? activityInfo.applicationInfo : null;
+        final String packageName = appInfo != null
+                ? appInfo.packageName : provider.id.componentName.getPackageName();
         final int appUserId = provider.getUserId();
         boolean showBadge = false;
 
@@ -750,7 +755,7 @@
             } else if (provider.maskedBySuspendedPackage) {
                 showBadge = mUserManager.hasBadge(appUserId);
                 final UserPackage suspendingPackage = mPackageManagerInternal.getSuspendingPackage(
-                        appInfo.packageName, appUserId);
+                        packageName, appUserId);
                 // TODO(b/281839596): don't rely on platform always meaning suspended by admin.
                 if (suspendingPackage != null
                         && PLATFORM_PACKAGE_NAME.equals(suspendingPackage.packageName)) {
@@ -759,11 +764,11 @@
                 } else {
                     final SuspendDialogInfo dialogInfo =
                             mPackageManagerInternal.getSuspendedDialogInfo(
-                                    appInfo.packageName, suspendingPackage, appUserId);
+                                    packageName, suspendingPackage, appUserId);
                     // onUnsuspend is null because we don't want to start any activity on
                     // unsuspending from a suspended widget.
                     onClickIntent = SuspendedAppActivity.createSuspendedAppInterceptIntent(
-                            appInfo.packageName, suspendingPackage, dialogInfo, null, null,
+                            packageName, suspendingPackage, dialogInfo, null, null,
                             appUserId);
                 }
             } else if (provider.maskedByLockedProfile) {
@@ -778,7 +783,7 @@
                 showBadge = mUserManager.hasBadge(appUserId);
             }
 
-            Icon icon = appInfo.icon != 0
+            Icon icon = (appInfo != null && appInfo.icon != 0)
                     ? Icon.createWithResource(appInfo.packageName, appInfo.icon)
                     : Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon);
             views.setImageViewIcon(R.id.work_widget_app_icon, icon);
@@ -2955,6 +2960,9 @@
             AppWidgetProviderInfo info = new AppWidgetProviderInfo();
             info.provider = providerId.componentName;
             info.providerInfo = ri.activityInfo;
+            if (DEBUG_NULL_PROVIDER_INFO) {
+                Objects.requireNonNull(ri.activityInfo);
+            }
             return info;
         }
         return null;
@@ -2989,6 +2997,9 @@
             AppWidgetProviderInfo info = new AppWidgetProviderInfo();
             info.provider = providerId.componentName;
             info.providerInfo = activityInfo;
+            if (DEBUG_NULL_PROVIDER_INFO) {
+                Objects.requireNonNull(activityInfo);
+            }
 
             final Resources resources;
             final long identity = Binder.clearCallingIdentity();
@@ -3564,6 +3575,9 @@
                             AppWidgetProviderInfo info = new AppWidgetProviderInfo();
                             info.provider = providerId.componentName;
                             info.providerInfo = providerInfo;
+                            if (DEBUG_NULL_PROVIDER_INFO) {
+                                Objects.requireNonNull(providerInfo);
+                            }
 
                             provider = new Provider();
                             provider.setPartialInfoLocked(info);
@@ -3580,6 +3594,9 @@
                             if (info != null) {
                                 info.provider = providerId.componentName;
                                 info.providerInfo = providerInfo;
+                                if (DEBUG_NULL_PROVIDER_INFO) {
+                                    Objects.requireNonNull(providerInfo);
+                                }
                                 provider.setInfoLocked(info);
                             }
                         }
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 763879e..6fc05b7 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -1998,6 +1998,19 @@
         }
 
         @Override
+        public void setViewAutofilled(int sessionId, @NonNull AutofillId id, int userId) {
+            synchronized (mLock) {
+                final AutofillManagerServiceImpl service =
+                        peekServiceForUserWithLocalBinderIdentityLocked(userId);
+                if (service != null) {
+                    service.setViewAutofilled(sessionId, getCallingUid(), id);
+                } else if (sVerbose) {
+                    Slog.v(TAG, "setAutofillFailure(): no service for " + userId);
+                }
+            }
+        }
+
+        @Override
         public void finishSession(int sessionId, int userId,
                 @AutofillCommitReason int commitReason) {
             synchronized (mLock) {
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 92acce2..588266f 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -466,6 +466,7 @@
     @GuardedBy("mLock")
     void setAutofillFailureLocked(int sessionId, int uid, @NonNull List<AutofillId> ids) {
         if (!isEnabledLocked()) {
+            Slog.wtf(TAG, "Service not enabled");
             return;
         }
         final Session session = mSessions.get(sessionId);
@@ -477,8 +478,23 @@
     }
 
     @GuardedBy("mLock")
+    void setViewAutofilled(int sessionId, int uid, @NonNull AutofillId id) {
+        if (!isEnabledLocked()) {
+            Slog.wtf(TAG, "Service not enabled");
+            return;
+        }
+        final Session session = mSessions.get(sessionId);
+        if (session == null || uid != session.uid) {
+            Slog.v(TAG, "setViewAutofilled(): no session for " + sessionId + "(" + uid + ")");
+            return;
+        }
+        session.setViewAutofilled(id);
+    }
+
+    @GuardedBy("mLock")
     void finishSessionLocked(int sessionId, int uid, @AutofillCommitReason int commitReason) {
         if (!isEnabledLocked()) {
+            Slog.wtf(TAG, "Service not enabled");
             return;
         }
 
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 9c84b12..f289115 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -65,6 +65,7 @@
 import android.provider.Settings;
 import android.service.autofill.Dataset;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Slog;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
@@ -548,9 +549,10 @@
     /**
      * Set views_fillable_total_count as long as mEventInternal presents.
      */
-    public void maybeSetViewFillableCounts(int totalFillableCount) {
+    public void maybeSetViewFillablesAndCount(List<AutofillId> autofillIds) {
         mEventInternal.ifPresent(event -> {
-            event.mViewFillableTotalCount = totalFillableCount;
+            event.mAutofillIdsAttemptedAutofill = new ArraySet<>(autofillIds);
+            event.mViewFillableTotalCount = event.mAutofillIdsAttemptedAutofill.size();
         });
     }
 
@@ -564,6 +566,41 @@
         });
     }
 
+    /** Sets focused_autofill_id using view id */
+    public void maybeSetFocusedId(AutofillId id) {
+        maybeSetFocusedId(id.getViewId());
+    }
+
+    /** Sets focused_autofill_id as long as mEventInternal is present */
+    public void maybeSetFocusedId(int id) {
+        mEventInternal.ifPresent(event -> {
+            event.mFocusedId = id;
+        });
+    }
+    /**
+     * Set views_filled_failure_count using failure count as long as mEventInternal
+     * presents.
+     */
+    public void maybeAddSuccessId(AutofillId autofillId) {
+        mEventInternal.ifPresent(event -> {
+            ArraySet<AutofillId> autofillIds = event.mAutofillIdsAttemptedAutofill;
+            if (autofillIds == null) {
+                Slog.w(TAG, "Attempted autofill ids is null, but received autofillId:" + autofillId
+                        + " successfully filled");
+                event.mViewFilledButUnexpectedCount++;
+            } else if (autofillIds.contains(autofillId)) {
+                if (sVerbose) {
+                    Slog.v(TAG, "Logging autofill for id:" + autofillId);
+                    event.mViewFillSuccessCount++;
+                }
+            } else {
+                Slog.w(TAG, "Successfully filled autofillId:" + autofillId
+                        + " not found in list of attempted autofill ids: " + autofillIds);
+                event.mViewFilledButUnexpectedCount++;
+            }
+        });
+    }
+
     public void logAndEndEvent() {
         if (!mEventInternal.isPresent()) {
             Slog.w(TAG, "Shouldn't be logging AutofillPresentationEventReported again for same "
@@ -608,7 +645,10 @@
                     + " mIsCredentialRequest=" + event.mIsCredentialRequest
                     + " mWebviewRequestedCredential=" + event.mWebviewRequestedCredential
                     + " mViewFillableTotalCount=" + event.mViewFillableTotalCount
-                    + " mViewFillFailureCount=" + event.mViewFillFailureCount);
+                    + " mViewFillFailureCount=" + event.mViewFillFailureCount
+                    + " mFocusedId=" + event.mFocusedId
+                    + " mViewFillSuccessCount=" + event.mViewFillSuccessCount
+                    + " mViewFilledButUnexpectedCount=" + event.mViewFilledButUnexpectedCount);
         }
 
         // TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -651,7 +691,10 @@
                 event.mIsCredentialRequest,
                 event.mWebviewRequestedCredential,
                 event.mViewFillableTotalCount,
-                event.mViewFillFailureCount);
+                event.mViewFillFailureCount,
+                event.mFocusedId,
+                event.mViewFillSuccessCount,
+                event.mViewFilledButUnexpectedCount);
         mEventInternal = Optional.empty();
     }
 
@@ -689,7 +732,14 @@
         boolean mWebviewRequestedCredential = false;
         int mViewFillableTotalCount = -1;
         int mViewFillFailureCount = -1;
+        int mFocusedId = -1;
 
+        // Default value for success count is set to 0 explicitly. Setting it to -1 for
+        // uninitialized doesn't help much, as this would be non-zero only if callback is received.
+        int mViewFillSuccessCount = 0;
+        int mViewFilledButUnexpectedCount = 0;
+
+        ArraySet<AutofillId> mAutofillIdsAttemptedAutofill;
         PresentationStatsEventInternal() {}
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 8b13c4b7..07b16c5 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -4669,6 +4669,7 @@
                 mFieldClassificationIdSnapshot);
         mPresentationStatsEventLogger.maybeSetAvailableCount(
                 response.getDatasets(), mCurrentViewId);
+        mPresentationStatsEventLogger.maybeSetFocusedId(mCurrentViewId);
     }
 
     @GuardedBy("mLock")
@@ -5381,7 +5382,20 @@
             }
         }
         mPresentationStatsEventLogger.maybeSetViewFillFailureCounts(ids.size());
-        mPresentationStatsEventLogger.logAndEndEvent();
+    }
+
+    /**
+     * Sets the state of views that failed to autofill.
+     */
+    @GuardedBy("mLock")
+    void setViewAutofilled(@NonNull AutofillId id) {
+        if (sVerbose) {
+            Slog.v(TAG, "View autofilled: " + id);
+        }
+        if (id.getSessionId() == AutofillId.NO_SESSION) {
+            id.setSessionId(this.id);
+        }
+        mPresentationStatsEventLogger.maybeAddSuccessId(id);
     }
 
     @GuardedBy("mLock")
@@ -6589,7 +6603,7 @@
                     if (sVerbose) {
                         Slog.v(TAG, "Total views to be autofilled: " + ids.size());
                     }
-                    mPresentationStatsEventLogger.maybeSetViewFillableCounts(ids.size());
+                    mPresentationStatsEventLogger.maybeSetViewFillablesAndCount(ids);
                     if (sDebug) Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset);
                     mClient.autofill(id, ids, values, hideHighlight);
                     if (dataset.getId() != null) {
diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java
index 70443f9..38a412f 100644
--- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java
+++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java
@@ -118,7 +118,7 @@
         final InputMethodManagerInternal inputMethodManagerInternal =
                 LocalServices.getService(InputMethodManagerInternal.class);
         if (!inputMethodManagerInternal.transferTouchFocusToImeWindow(sourceInputToken,
-                displayId)) {
+                displayId, mUserId)) {
             Slog.e(TAG, "Cannot transfer touch focus from suggestion to IME");
             mOnErrorCallback.run();
         }
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 9b72288..70af49c 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -41,7 +41,6 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
-import android.view.Display;
 import android.view.InputDevice;
 import android.view.WindowManager;
 
@@ -169,7 +168,6 @@
         createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
                 deviceToken, displayId, phys,
                 () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
-        setVirtualMousePointerDisplayId(displayId);
     }
 
     void createTouchscreen(@NonNull String deviceName, int vendorId, int productId,
@@ -226,7 +224,7 @@
         token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0);
         mNativeWrapper.closeUinput(inputDeviceDescriptor.getNativePointer());
         String phys = inputDeviceDescriptor.getPhys();
-        InputManagerGlobal.getInstance().removeUniqueIdAssociation(phys);
+        InputManagerGlobal.getInstance().removeUniqueIdAssociationByPort(phys);
         // Type associations are added in the case of navigation touchpads. Those should be removed
         // once the input device gets closed.
         if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) {
@@ -236,15 +234,6 @@
         if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_KEYBOARD) {
             mInputManagerInternal.removeKeyboardLayoutAssociation(phys);
         }
-
-        // Reset values to the default if all virtual mice are unregistered, or set display
-        // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be
-        // removed from the mInputDeviceDescriptors instance variable prior to this point.
-        if (inputDeviceDescriptor.isMouse()) {
-            if (getVirtualMousePointerDisplayId() == inputDeviceDescriptor.getDisplayId()) {
-                updateActivePointerDisplayIdLocked();
-            }
-        }
     }
 
     /**
@@ -276,29 +265,6 @@
         mWindowManager.setDisplayImePolicy(displayId, policy);
     }
 
-    // TODO(b/293587049): Remove after pointer icon refactor is complete.
-    @GuardedBy("mLock")
-    private void updateActivePointerDisplayIdLocked() {
-        InputDeviceDescriptor mostRecentlyCreatedMouse = null;
-        for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
-            InputDeviceDescriptor otherInputDeviceDescriptor = mInputDeviceDescriptors.valueAt(i);
-            if (otherInputDeviceDescriptor.isMouse()) {
-                if (mostRecentlyCreatedMouse == null
-                        || (otherInputDeviceDescriptor.getCreationOrderNumber()
-                        > mostRecentlyCreatedMouse.getCreationOrderNumber())) {
-                    mostRecentlyCreatedMouse = otherInputDeviceDescriptor;
-                }
-            }
-        }
-        if (mostRecentlyCreatedMouse != null) {
-            setVirtualMousePointerDisplayId(
-                    mostRecentlyCreatedMouse.getDisplayId());
-        } else {
-            // All mice have been unregistered
-            setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY);
-        }
-    }
-
     /**
      * Validates a device name by checking whether a device with the same name already exists.
      * @param deviceName The name of the device to be validated
@@ -321,7 +287,7 @@
 
     private void setUniqueIdAssociation(int displayId, String phys) {
         final String displayUniqueId = mDisplayManagerInternal.getDisplayInfo(displayId).uniqueId;
-        InputManagerGlobal.getInstance().addUniqueIdAssociation(phys, displayUniqueId);
+        InputManagerGlobal.getInstance().addUniqueIdAssociationByPort(phys, displayUniqueId);
     }
 
     boolean sendDpadKeyEvent(@NonNull IBinder token, @NonNull VirtualKeyEvent event) {
@@ -355,9 +321,6 @@
             if (inputDeviceDescriptor == null) {
                 return false;
             }
-            if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
-                setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
-            }
             return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(),
                     event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
         }
@@ -384,9 +347,6 @@
             if (inputDeviceDescriptor == null) {
                 return false;
             }
-            if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
-                setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
-            }
             return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(),
                     event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos());
         }
@@ -399,9 +359,6 @@
             if (inputDeviceDescriptor == null) {
                 return false;
             }
-            if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
-                setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
-            }
             return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(),
                     event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos());
         }
@@ -415,9 +372,6 @@
                 throw new IllegalArgumentException(
                         "Could not get cursor position for input device for given token");
             }
-            if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
-                setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
-            }
             return LocalServices.getService(InputManagerInternal.class).getCursorPosition(
                     inputDeviceDescriptor.getDisplayId());
         }
@@ -835,7 +789,7 @@
                 throw e;
             }
         } catch (DeviceCreationException e) {
-            InputManagerGlobal.getInstance().removeUniqueIdAssociation(phys);
+            InputManagerGlobal.getInstance().removeUniqueIdAssociationByPort(phys);
             throw e;
         }
 
@@ -878,22 +832,4 @@
         /** Returns true if the calling thread is a valid thread for device creation. */
         boolean isValidThread();
     }
-
-    // TODO(b/293587049): Remove after pointer icon refactor is complete.
-    private void setVirtualMousePointerDisplayId(int displayId) {
-        if (com.android.input.flags.Flags.enablePointerChoreographer()) {
-            // We no longer need to set the pointer display when pointer choreographer is enabled.
-            return;
-        }
-        mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
-    }
-
-    // TODO(b/293587049): Remove after pointer icon refactor is complete.
-    private int getVirtualMousePointerDisplayId() {
-        if (com.android.input.flags.Flags.enablePointerChoreographer()) {
-            // We no longer need to get the pointer display when pointer choreographer is enabled.
-            return Display.INVALID_DISPLAY;
-        }
-        return mInputManagerInternal.getVirtualMousePointerDisplayId();
-    }
 }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3ff0504..d153c18 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -258,6 +258,8 @@
         "core_os_flags_lib",
         "connectivity_flags_lib",
         "dreams_flags_lib",
+        "aconfig_new_storage_flags_lib",
+        "aconfigd_java_proto_lib",
     ],
     javac_shard_size: 50,
     javacflags: [
diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java
index 748253f..1a8c3b0 100644
--- a/services/core/java/com/android/server/DropBoxManagerService.java
+++ b/services/core/java/com/android/server/DropBoxManagerService.java
@@ -35,7 +35,6 @@
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.BundleMerger;
 import android.os.Debug;
 import android.os.DropBoxManager;
@@ -176,6 +175,16 @@
         }
     };
 
+    private static final BundleMerger sDropboxEntryAddedExtrasMerger;
+    static {
+        sDropboxEntryAddedExtrasMerger = new BundleMerger();
+        sDropboxEntryAddedExtrasMerger.setDefaultMergeStrategy(BundleMerger.STRATEGY_FIRST);
+        sDropboxEntryAddedExtrasMerger.setMergeStrategy(DropBoxManager.EXTRA_TIME,
+                BundleMerger.STRATEGY_COMPARABLE_MAX);
+        sDropboxEntryAddedExtrasMerger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT,
+                BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD);
+    }
+
     private final IDropBoxManagerService.Stub mStub = new IDropBoxManagerService.Stub() {
         @Override
         public void addData(String tag, byte[] data, int flags) {
@@ -284,7 +293,7 @@
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_SEND_BROADCAST:
-                    prepareAndSendBroadcast((Intent) msg.obj, null);
+                    prepareAndSendBroadcast((Intent) msg.obj, false);
                     break;
                 case MSG_SEND_DEFERRED_BROADCAST:
                     Intent deferredIntent;
@@ -292,31 +301,42 @@
                         deferredIntent = mDeferredMap.remove((String) msg.obj);
                     }
                     if (deferredIntent != null) {
-                        prepareAndSendBroadcast(deferredIntent,
-                                createBroadcastOptions(deferredIntent));
+                        prepareAndSendBroadcast(deferredIntent, true);
                     }
                     break;
             }
         }
 
-        private void prepareAndSendBroadcast(Intent intent, Bundle options) {
+        private void prepareAndSendBroadcast(Intent intent, boolean deferrable) {
             if (!DropBoxManagerService.this.mBooted) {
                 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
             }
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
             if (Flags.enableReadDropboxPermission()) {
-                BroadcastOptions unbundledOptions = (options == null)
-                        ? BroadcastOptions.makeBasic() : BroadcastOptions.fromBundle(options);
-
-                unbundledOptions.setRequireCompatChange(ENFORCE_READ_DROPBOX_DATA, true);
+                options.setRequireCompatChange(ENFORCE_READ_DROPBOX_DATA, true);
+                if (deferrable) {
+                    final String matchingKey = intent.getStringExtra(DropBoxManager.EXTRA_TAG)
+                            + "-READ_DROPBOX_DATA";
+                    setBroadcastOptionsForDeferral(options, matchingKey);
+                }
                 getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
-                        Manifest.permission.READ_DROPBOX_DATA, unbundledOptions.toBundle());
+                        Manifest.permission.READ_DROPBOX_DATA, options.toBundle());
 
-                unbundledOptions.setRequireCompatChange(ENFORCE_READ_DROPBOX_DATA, false);
+                options.setRequireCompatChange(ENFORCE_READ_DROPBOX_DATA, false);
+                if (deferrable) {
+                    final String matchingKey = intent.getStringExtra(DropBoxManager.EXTRA_TAG)
+                            + "-READ_LOGS";
+                    setBroadcastOptionsForDeferral(options, matchingKey);
+                }
                 getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
-                        Manifest.permission.READ_LOGS, unbundledOptions.toBundle());
+                        Manifest.permission.READ_LOGS, options.toBundle());
             } else {
+                if (deferrable) {
+                    final String matchingKey = intent.getStringExtra(DropBoxManager.EXTRA_TAG);
+                    setBroadcastOptionsForDeferral(options, matchingKey);
+                }
                 getContext().sendBroadcastAsUser(intent, UserHandle.ALL,
-                        android.Manifest.permission.READ_LOGS, options);
+                        android.Manifest.permission.READ_LOGS, options.toBundle());
             }
         }
 
@@ -328,21 +348,12 @@
             return dropboxIntent;
         }
 
-        private Bundle createBroadcastOptions(Intent intent) {
-            final BundleMerger extrasMerger = new BundleMerger();
-            extrasMerger.setDefaultMergeStrategy(BundleMerger.STRATEGY_FIRST);
-            extrasMerger.setMergeStrategy(DropBoxManager.EXTRA_TIME,
-                    BundleMerger.STRATEGY_COMPARABLE_MAX);
-            extrasMerger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT,
-                    BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD);
-
-            return BroadcastOptions.makeBasic()
-                    .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED)
+        private void setBroadcastOptionsForDeferral(BroadcastOptions options, String matchingKey) {
+            options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED)
                     .setDeliveryGroupMatchingKey(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED,
-                            intent.getStringExtra(DropBoxManager.EXTRA_TAG))
-                    .setDeliveryGroupExtrasMerger(extrasMerger)
-                    .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
-                    .toBundle();
+                            matchingKey)
+                    .setDeliveryGroupExtrasMerger(sDropboxEntryAddedExtrasMerger)
+                    .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE);
         }
 
         /**
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 7acca19..e2b6bd6 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -36,6 +36,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.app.AppOpsManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -47,6 +48,7 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
+import android.net.vcn.Flags;
 import android.net.vcn.IVcnManagementService;
 import android.net.vcn.IVcnStatusCallback;
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
@@ -68,6 +70,7 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
@@ -431,6 +434,8 @@
         mTelephonySubscriptionTracker.register();
     }
 
+    // The system server automatically has the required permissions for #getMainUser()
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     private void enforcePrimaryUser() {
         final int uid = mDeps.getBinderCallingUid();
         if (uid == Process.SYSTEM_UID) {
@@ -438,7 +443,20 @@
                     "Calling identity was System Server. Was Binder calling identity cleared?");
         }
 
-        if (!UserHandle.getUserHandleForUid(uid).isSystem()) {
+        final UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+
+        if (Flags.enforceMainUser()) {
+            final UserManager userManager = mContext.getSystemService(UserManager.class);
+
+            Binder.withCleanCallingIdentity(
+                    () -> {
+                        if (!Objects.equals(userManager.getMainUser(), userHandle)) {
+                            throw new SecurityException(
+                                    "VcnManagementService can only be used by callers running as"
+                                            + " the main user");
+                        }
+                    });
+        } else if (!userHandle.isSystem()) {
             throw new SecurityException(
                     "VcnManagementService can only be used by callers running as the primary user");
         }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 408cff0..94bf813 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -1169,9 +1169,7 @@
     }
 
     private boolean shouldAllowBootCompletedStart(ServiceRecord r, int foregroundServiceType) {
-        @PowerExemptionManager.ReasonCode final int fgsStartReasonCode =
-                r.mInfoTempFgsAllowListReason != null ? r.mInfoTempFgsAllowListReason.mReasonCode
-                                                      : REASON_DENIED;
+        @PowerExemptionManager.ReasonCode final int fgsStartReasonCode = r.getFgsAllowStart();
         if (Flags.fgsBootCompleted()
                 && CompatChanges.isChangeEnabled(FGS_BOOT_COMPLETED_RESTRICTIONS, r.appInfo.uid)
                 && fgsStartReasonCode == PowerExemptionManager.REASON_BOOT_COMPLETED) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 20c3e7f..46ed1fd 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4781,7 +4781,7 @@
 
             checkTime(startTime, "attachApplicationLocked: immediately before bindApplication");
             bindApplicationTimeMillis = SystemClock.uptimeMillis();
-            bindApplicationTimeNanos = SystemClock.elapsedRealtimeNanos();
+            bindApplicationTimeNanos = SystemClock.uptimeNanos();
             mAtmInternal.preBindApplication(app.getWindowProcessController());
             final ActiveInstrumentation instr2 = app.getActiveInstrumentation();
             if (mPlatformCompat != null) {
@@ -4851,7 +4851,8 @@
 
             app.setBindApplicationTime(bindApplicationTimeMillis);
             mProcessList.getAppStartInfoTracker()
-                    .reportBindApplicationTimeNanos(app, bindApplicationTimeNanos);
+                    .addTimestampToStart(app, bindApplicationTimeNanos,
+                            ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION);
 
             // Make app active after binding application or client may be running requests (e.g
             // starting activities) before it is ready.
@@ -14317,7 +14318,7 @@
     // activity manager to announce its creation.
     public boolean bindBackupAgent(String packageName, int backupMode, int targetUserId,
             @BackupDestination int backupDestination) {
-        long startTimeNs = SystemClock.elapsedRealtimeNanos();
+        long startTimeNs = SystemClock.uptimeNanos();
         if (DEBUG_BACKUP) {
             Slog.v(TAG, "bindBackupAgent: app=" + packageName + " mode=" + backupMode
                     + " targetUserId=" + targetUserId + " callingUid = " + Binder.getCallingUid()
@@ -19416,7 +19417,6 @@
                     // If the process is known as top app, set a hint so when the process is
                     // started, the top priority can be applied immediately to avoid cpu being
                     // preempted by other processes before attaching the process of top app.
-                    final long startTimeNs = SystemClock.elapsedRealtimeNanos();
                     HostingRecord hostingRecord =
                             new HostingRecord(hostingType, hostingName, isTop);
                     ProcessRecord rec = getProcessRecordLocked(processName, info.uid);
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 3cea014..a182a10 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -115,6 +115,7 @@
 import android.util.ArraySet;
 import android.util.DebugUtils;
 import android.util.DisplayMetrics;
+import android.util.SparseArray;
 import android.util.TeeWriter;
 import android.util.proto.ProtoOutputStream;
 import android.view.Choreographer;
@@ -275,9 +276,9 @@
                 case "compact":
                     return runCompact(pw);
                 case "freeze":
-                    return runFreeze(pw);
+                    return runFreeze(pw, true);
                 case "unfreeze":
-                    return runUnfreeze(pw);
+                    return runFreeze(pw, false);
                 case "instrument":
                     getOutPrintWriter().println("Error: must be invoked through 'am instrument'.");
                     return -1;
@@ -1203,45 +1204,27 @@
     }
 
     @NeverCompile
-    int runFreeze(PrintWriter pw) throws RemoteException {
+    int runFreeze(PrintWriter pw, boolean freeze) throws RemoteException {
         String freezerOpt = getNextOption();
         boolean isSticky = false;
-        if (freezerOpt != null) {
-            isSticky = freezerOpt.equals("--sticky");
-        }
-        ProcessRecord app = getProcessFromShell();
-        if (app == null) {
-            getErrPrintWriter().println("Error: could not find process");
-            return -1;
-        }
-        pw.println("Freezing pid: " + app.mPid + " sticky=" + isSticky);
-        synchronized (mInternal) {
-            synchronized (mInternal.mProcLock) {
-                app.mOptRecord.setFreezeSticky(isSticky);
-                mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app);
-            }
-        }
-        return 0;
-    }
 
-    @NeverCompile
-    int runUnfreeze(PrintWriter pw) throws RemoteException {
-        String freezerOpt = getNextOption();
-        boolean isSticky = false;
         if (freezerOpt != null) {
             isSticky = freezerOpt.equals("--sticky");
         }
-        ProcessRecord app = getProcessFromShell();
-        if (app == null) {
-            getErrPrintWriter().println("Error: could not find process");
+        ProcessRecord proc = getProcessFromShell();
+        if (proc == null) {
             return -1;
         }
-        pw.println("Unfreezing pid: " + app.mPid);
+        pw.print(freeze ? "Freezing" : "Unfreezing");
+        pw.print(" process " + proc.processName);
+        pw.println(" (" + proc.mPid + ") sticky=" + isSticky);
         synchronized (mInternal) {
             synchronized (mInternal.mProcLock) {
-                synchronized (mInternal.mOomAdjuster.mCachedAppOptimizer.mFreezerLock) {
-                    app.mOptRecord.setFreezeSticky(isSticky);
-                    mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(app, 0,
+                proc.mOptRecord.setFreezeSticky(isSticky);
+                if (freeze) {
+                    mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(proc);
+                } else {
+                    mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(proc, 0,
                             true);
                 }
             }
@@ -1250,43 +1233,42 @@
     }
 
     /**
-     * Parses from the shell the process name and user id if provided and provides the corresponding
-     * {@link ProcessRecord)} If no user is provided, it will fallback to current user.
-     * Example usage: {@code <processname> --user current} or {@code <processname>}
-     * @return process record of process, null if none found.
+     * Parses from the shell the pid or process name and provides the corresponding
+     * {@link ProcessRecord}.
+     * Example usage: {@code <processname>} or {@code <pid>}
+     * @return process record of process, null if none or more than one found.
      * @throws RemoteException
      */
     @NeverCompile
     ProcessRecord getProcessFromShell() throws RemoteException {
-        ProcessRecord app;
-        String processName = getNextArgRequired();
-        synchronized (mInternal.mProcLock) {
-            // Default to current user
-            int userId = getUserIdFromShellOrFallback();
-            final int uid =
-                    mInternal.getPackageManagerInternal().getPackageUid(processName, 0, userId);
-            app = mInternal.getProcessRecordLocked(processName, uid);
+        ProcessRecord proc = null;
+        String process = getNextArgRequired();
+        try {
+            int pid = Integer.parseInt(process);
+            synchronized (mInternal.mPidsSelfLocked) {
+                proc = mInternal.mPidsSelfLocked.get(pid);
+            }
+        } catch (NumberFormatException e) {
+            // Fallback to process name if it's not a valid pid
         }
-        return app;
-    }
 
-    /**
-     * @return User id from command line provided in the form of
-     *  {@code --user <userid|current|all>} and if the argument is not found it will fallback
-     *  to current user.
-     * @throws RemoteException
-     */
-    @NeverCompile
-    int getUserIdFromShellOrFallback() throws RemoteException {
-        int userId = mInterface.getCurrentUserId();
-        String userOpt = getNextOption();
-        if (userOpt != null && "--user".equals(userOpt)) {
-            int inputUserId = UserHandle.parseUserArg(getNextArgRequired());
-            if (inputUserId != UserHandle.USER_CURRENT) {
-                userId = inputUserId;
+        if (proc == null) {
+            synchronized (mInternal.mProcLock) {
+                ArrayMap<String, SparseArray<ProcessRecord>> all =
+                        mInternal.mProcessList.getProcessNamesLOSP().getMap();
+                SparseArray<ProcessRecord> procs = all.get(process);
+                if (procs == null || procs.size() == 0) {
+                    getErrPrintWriter().println("Error: could not find process");
+                    return null;
+                } else if (procs.size() > 1) {
+                    getErrPrintWriter().println("Error: more than one processes found");
+                    return null;
+                }
+                proc = procs.valueAt(0);
             }
         }
-        return userId;
+
+        return proc;
     }
 
     int runDumpHeap(PrintWriter pw) throws RemoteException {
@@ -4306,24 +4288,26 @@
             pw.println("      --allow-background-activity-starts: The receiver may start activities");
             pw.println("          even if in the background.");
             pw.println("      --async: Send without waiting for the completion of the receiver.");
-            pw.println("  compact [some|full] <process_name> [--user <USER_ID>]");
-            pw.println("      Perform a single process compaction.");
+            pw.println("  compact {some|full} <PROCESS>");
+            pw.println("      Perform a single process compaction. The given <PROCESS> argument");
+            pw.println("          may be either a process name or pid.");
             pw.println("      some: execute file compaction.");
             pw.println("      full: execute anon + file compaction.");
-            pw.println("      system: system compaction.");
             pw.println("  compact system");
             pw.println("      Perform a full system compaction.");
-            pw.println("  compact native [some|full] <pid>");
+            pw.println("  compact native {some|full} <pid>");
             pw.println("      Perform a native compaction for process with <pid>.");
             pw.println("      some: execute file compaction.");
             pw.println("      full: execute anon + file compaction.");
-            pw.println("  freeze [--sticky] <processname> [--user <USER_ID>]");
-            pw.println("      Freeze a process.");
-            pw.println("        --sticky: persists the frozen state for the process lifetime or");
+            pw.println("  freeze [--sticky] <PROCESS>");
+            pw.println("      Freeze a process. The given <PROCESS> argument");
+            pw.println("          may be either a process name or pid.  Options are:");
+            pw.println("      --sticky: persists the frozen state for the process lifetime or");
             pw.println("                  until an unfreeze is triggered via shell");
-            pw.println("  unfreeze [--sticky] <processname> [--user <USER_ID>]");
-            pw.println("      Unfreeze a process.");
-            pw.println("        --sticky: persists the unfrozen state for the process lifetime or");
+            pw.println("  unfreeze [--sticky] <PROCESS>");
+            pw.println("      Unfreeze a process. The given <PROCESS> argument");
+            pw.println("          may be either a process name or pid.  Options are:");
+            pw.println("      --sticky: persists the unfrozen state for the process lifetime or");
             pw.println("                  until a freeze is triggered via shell");
             pw.println("  instrument [-r] [-e <NAME> <VALUE>] [-p <FILE>] [-w]");
             pw.println("          [--user <USER_ID> | current]");
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 7583657..79a8518 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -426,14 +426,6 @@
         }
     }
 
-    void reportApplicationOnCreateTimeNanos(ProcessRecord app, long timeNs) {
-        if (!mEnabled) {
-            return;
-        }
-        addTimestampToStart(app, timeNs,
-                ApplicationStartInfo.START_TIMESTAMP_APPLICATION_ONCREATE);
-    }
-
     /**
      * Helper functions for monitoring shell command.
      * > adb shell am start-info-detailed-monitoring [package-name]
@@ -468,41 +460,14 @@
         }
     }
 
-    /** Report a bind application timestamp to add to {@link ApplicationStartInfo}. */
-    public void reportBindApplicationTimeNanos(ProcessRecord app, long timeNs) {
-        addTimestampToStart(app, timeNs,
-                ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION);
-    }
-
-    void reportFirstFrameTimeNanos(ProcessRecord app, long timeNs) {
-        if (!mEnabled) {
-            return;
-        }
-        addTimestampToStart(app, timeNs,
-                ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME);
-    }
-
-    void reportFullyDrawnTimeNanos(ProcessRecord app, long timeNs) {
-        if (!mEnabled) {
-            return;
-        }
-        addTimestampToStart(app, timeNs,
-                ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN);
-    }
-
-    void reportFullyDrawnTimeNanos(String processName, int uid, long timeNs) {
-        if (!mEnabled) {
-            return;
-        }
-        addTimestampToStart(processName, uid, timeNs,
-                ApplicationStartInfo.START_TIMESTAMP_FULLY_DRAWN);
-    }
-
-    private void addTimestampToStart(ProcessRecord app, long timeNs, int key) {
+    void addTimestampToStart(ProcessRecord app, long timeNs, int key) {
         addTimestampToStart(app.info.packageName, app.uid, timeNs, key);
     }
 
     void addTimestampToStart(String packageName, int uid, long timeNs, int key) {
+        if (!mEnabled) {
+            return;
+        }
         synchronized (mLock) {
             AppStartInfoContainer container = mData.get(packageName, uid);
             if (container == null) {
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 4425a38..1379c9b 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -1021,6 +1021,7 @@
         final boolean allowWhileBooting = (r.intent.getFlags()
                 & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0;
 
+        long startTimeNs = SystemClock.uptimeNanos();
         if (DEBUG_BROADCAST) logv("Scheduling " + r + " to cold " + queue);
         queue.app = mService.startProcessLocked(queue.processName, info, true, intentFlags,
                 hostingRecord, zygotePolicyFlags, allowWhileBooting, false);
@@ -1032,8 +1033,7 @@
         }
         // TODO: b/335420031 - cache receiver intent to avoid multiple calls to getReceiverIntent.
         mService.mProcessList.getAppStartInfoTracker().handleProcessBroadcastStart(
-                SystemClock.elapsedRealtimeNanos(), queue.app, r.getReceiverIntent(receiver),
-                r.alarm /* isAlarm */);
+                startTimeNs, queue.app, r.getReceiverIntent(receiver), r.alarm /* isAlarm */);
         return false;
     }
 
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 28fd197..a8b9e43 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -179,7 +179,7 @@
         final int expectedUserId = userId;
         synchronized (mService) {
             long startTime = SystemClock.uptimeMillis();
-            long startElapsedTimeNs = SystemClock.elapsedRealtimeNanos();
+            long startTimeNs = SystemClock.uptimeNanos();
 
             ProcessRecord r = null;
             if (caller != null) {
@@ -587,7 +587,7 @@
                                     firstLaunch,
                                     0L /* TODO: stoppedDuration */);
                             mService.mProcessList.getAppStartInfoTracker()
-                                    .handleProcessContentProviderStart(startElapsedTimeNs, proc);
+                                    .handleProcessContentProviderStart(startTimeNs, proc);
                         }
                         cpr.launchingApp = proc;
                         mLaunchingProviders.add(cpr);
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index a289dd1..8647750 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2610,6 +2610,15 @@
                 }
             }
 
+            // Sandbox should be able to control audio only when bound client
+            // has this capability.
+            if ((cstate.getCurCapability()
+                    & PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL) != 0) {
+                if (app.isSdkSandbox) {
+                    capability |= PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL;
+                }
+            }
+
             if (couldRecurse && shouldSkipDueToCycle(app, cstate, procState, adj, cycleReEval)) {
                 return false;
             }
@@ -3268,7 +3277,13 @@
         }
 
         final int curSchedGroup = state.getCurrentSchedulingGroup();
-        if (state.getSetSchedGroup() != curSchedGroup) {
+        if (app.getWaitingToKill() != null && app.mReceivers.numberOfCurReceivers() == 0
+                && ActivityManager.isProcStateBackground(state.getCurProcState())
+                && !state.hasStartedServices()) {
+            app.killLocked(app.getWaitingToKill(), ApplicationExitInfo.REASON_USER_REQUESTED,
+                    ApplicationExitInfo.SUBREASON_REMOVE_TASK, true);
+            success = false;
+        } else if (state.getSetSchedGroup() != curSchedGroup) {
             int oldSchedGroup = state.getSetSchedGroup();
             state.setSetSchedGroup(curSchedGroup);
             if (DEBUG_SWITCH || DEBUG_OOM_ADJ || mService.mCurOomAdjUid == app.uid) {
@@ -3276,74 +3291,66 @@
                         + " to " + curSchedGroup + ": " + state.getAdjType();
                 reportOomAdjMessageLocked(TAG_OOM_ADJ, msg);
             }
-            if (app.getWaitingToKill() != null && app.mReceivers.numberOfCurReceivers() == 0
-                    && ActivityManager.isProcStateBackground(state.getSetProcState())
-                    && !state.hasStartedServices()) {
-                app.killLocked(app.getWaitingToKill(), ApplicationExitInfo.REASON_USER_REQUESTED,
-                        ApplicationExitInfo.SUBREASON_REMOVE_TASK, true);
-                success = false;
-            } else {
-                int processGroup;
-                switch (curSchedGroup) {
-                    case SCHED_GROUP_BACKGROUND:
-                        processGroup = THREAD_GROUP_BACKGROUND;
-                        break;
-                    case SCHED_GROUP_TOP_APP:
-                    case SCHED_GROUP_TOP_APP_BOUND:
-                        processGroup = THREAD_GROUP_TOP_APP;
-                        break;
-                    case SCHED_GROUP_RESTRICTED:
-                        processGroup = THREAD_GROUP_RESTRICTED;
-                        break;
-                    default:
-                        processGroup = THREAD_GROUP_DEFAULT;
-                        break;
-                }
-                mProcessGroupHandler.sendMessage(mProcessGroupHandler.obtainMessage(
-                        0 /* unused */, app.getPid(), processGroup, app.processName));
-                try {
-                    final int renderThreadTid = app.getRenderThreadTid();
-                    if (curSchedGroup == SCHED_GROUP_TOP_APP) {
-                        // do nothing if we already switched to RT
-                        if (oldSchedGroup != SCHED_GROUP_TOP_APP) {
-                            app.getWindowProcessController().onTopProcChanged();
-                            if (app.useFifoUiScheduling()) {
-                                // Switch UI pipeline for app to SCHED_FIFO
-                                state.setSavedPriority(Process.getThreadPriority(app.getPid()));
-                                ActivityManagerService.setFifoPriority(app, true /* enable */);
-                            } else {
-                                // Boost priority for top app UI and render threads
-                                setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST);
-                                if (renderThreadTid != 0) {
-                                    try {
-                                        setThreadPriority(renderThreadTid,
-                                                THREAD_PRIORITY_TOP_APP_BOOST);
-                                    } catch (IllegalArgumentException e) {
-                                        // thread died, ignore
-                                    }
+            int processGroup;
+            switch (curSchedGroup) {
+                case SCHED_GROUP_BACKGROUND:
+                    processGroup = THREAD_GROUP_BACKGROUND;
+                    break;
+                case SCHED_GROUP_TOP_APP:
+                case SCHED_GROUP_TOP_APP_BOUND:
+                    processGroup = THREAD_GROUP_TOP_APP;
+                    break;
+                case SCHED_GROUP_RESTRICTED:
+                    processGroup = THREAD_GROUP_RESTRICTED;
+                    break;
+                default:
+                    processGroup = THREAD_GROUP_DEFAULT;
+                    break;
+            }
+            mProcessGroupHandler.sendMessage(mProcessGroupHandler.obtainMessage(
+                    0 /* unused */, app.getPid(), processGroup, app.processName));
+            try {
+                final int renderThreadTid = app.getRenderThreadTid();
+                if (curSchedGroup == SCHED_GROUP_TOP_APP) {
+                    // do nothing if we already switched to RT
+                    if (oldSchedGroup != SCHED_GROUP_TOP_APP) {
+                        app.getWindowProcessController().onTopProcChanged();
+                        if (app.useFifoUiScheduling()) {
+                            // Switch UI pipeline for app to SCHED_FIFO
+                            state.setSavedPriority(Process.getThreadPriority(app.getPid()));
+                            ActivityManagerService.setFifoPriority(app, true /* enable */);
+                        } else {
+                            // Boost priority for top app UI and render threads
+                            setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST);
+                            if (renderThreadTid != 0) {
+                                try {
+                                    setThreadPriority(renderThreadTid,
+                                            THREAD_PRIORITY_TOP_APP_BOOST);
+                                } catch (IllegalArgumentException e) {
+                                    // thread died, ignore
                                 }
                             }
                         }
-                    } else if (oldSchedGroup == SCHED_GROUP_TOP_APP
-                            && curSchedGroup != SCHED_GROUP_TOP_APP) {
-                        app.getWindowProcessController().onTopProcChanged();
-                        if (app.useFifoUiScheduling()) {
-                            // Reset UI pipeline to SCHED_OTHER
-                            ActivityManagerService.setFifoPriority(app, false /* enable */);
-                            setThreadPriority(app.getPid(), state.getSavedPriority());
-                        } else {
-                            // Reset priority for top app UI and render threads
-                            setThreadPriority(app.getPid(), 0);
-                        }
+                    }
+                } else if (oldSchedGroup == SCHED_GROUP_TOP_APP
+                        && curSchedGroup != SCHED_GROUP_TOP_APP) {
+                    app.getWindowProcessController().onTopProcChanged();
+                    if (app.useFifoUiScheduling()) {
+                        // Reset UI pipeline to SCHED_OTHER
+                        ActivityManagerService.setFifoPriority(app, false /* enable */);
+                        setThreadPriority(app.getPid(), state.getSavedPriority());
+                    } else {
+                        // Reset priority for top app UI and render threads
+                        setThreadPriority(app.getPid(), 0);
+                    }
 
-                        if (renderThreadTid != 0) {
-                            setThreadPriority(renderThreadTid, THREAD_PRIORITY_DISPLAY);
-                        }
+                    if (renderThreadTid != 0) {
+                        setThreadPriority(renderThreadTid, THREAD_PRIORITY_DISPLAY);
                     }
-                } catch (Exception e) {
-                    if (DEBUG_ALL) {
-                        Slog.w(TAG, "Failed setting thread priority of " + app.getPid(), e);
-                    }
+                }
+            } catch (Exception e) {
+                if (DEBUG_ALL) {
+                    Slog.w(TAG, "Failed setting thread priority of " + app.getPid(), e);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9520621..827db57 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -20,6 +20,8 @@
 import android.content.ContentResolver;
 import android.database.ContentObserver;
 import android.net.Uri;
+import android.net.LocalSocketAddress;
+import android.net.LocalSocket;
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.SystemProperties;
@@ -30,6 +32,14 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import android.aconfigd.Aconfigd.StorageRequestMessage;
+import android.aconfigd.Aconfigd.StorageRequestMessages;
+import android.aconfigd.Aconfigd.StorageReturnMessage;
+import android.aconfigd.Aconfigd.StorageReturnMessages;
+import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
@@ -185,6 +195,7 @@
         "pmw",
         "power",
         "preload_safety",
+        "printing",
         "privacy_infra_policy",
         "resource_manager",
         "responsible_apis",
@@ -224,6 +235,8 @@
     public static final String NAMESPACE_REBOOT_STAGING = "staged";
     public static final String NAMESPACE_REBOOT_STAGING_DELIMITER = "*";
 
+    public static final String NAMESPACE_LOCAL_OVERRIDES = "device_config_overrides";
+
     private final String[] mGlobalSettings;
 
     private final String[] mDeviceConfigScopes;
@@ -329,6 +342,7 @@
               HashMap<String, HashMap<String, String>> propsToStage =
                   getStagedFlagsWithValueChange(properties);
 
+              // send prop stage request to sys prop
               for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) {
                 String actualNamespace = entry.getKey();
                 HashMap<String, String> flagValuesToStage = entry.getValue();
@@ -349,7 +363,118 @@
                 }
               }
 
-            });
+              // send prop stage request to new storage
+              if (enableAconfigStorageDaemon()) {
+                  stageFlagsInNewStorage(propsToStage);
+              }
+
+        });
+
+        // add prop sync callback for flag local overrides
+        DeviceConfig.addOnPropertiesChangedListener(
+            NAMESPACE_LOCAL_OVERRIDES,
+            AsyncTask.THREAD_POOL_EXECUTOR,
+            (DeviceConfig.Properties properties) -> {
+                if (enableAconfigStorageDaemon()) {
+                    setLocalOverridesInNewStorage(properties);
+                }
+        });
+    }
+
+    /**
+     * apply flag local override in aconfig new storage
+     * @param props
+     * @return aconfigd socket return
+     */
+    public static StorageReturnMessages sendAconfigdRequests(StorageRequestMessages requests) {
+        // connect to aconfigd socket
+        LocalSocket client = new LocalSocket();
+        try{
+            client.connect(new LocalSocketAddress(
+                "aconfigd", LocalSocketAddress.Namespace.RESERVED));
+            log("connected to aconfigd socket");
+        } catch (IOException ioe) {
+            log("failed to connect to aconfigd socket", ioe);
+            return null;
+        }
+
+        DataInputStream inputStream = null;
+        DataOutputStream outputStream = null;
+        try {
+            inputStream = new DataInputStream(client.getInputStream());
+            outputStream = new DataOutputStream(client.getOutputStream());
+        } catch (IOException ioe) {
+            log("failed to get local socket iostreams", ioe);
+            return null;
+        }
+
+        // send requests
+        try {
+            byte[] requests_bytes = requests.toByteArray();
+            outputStream.writeInt(requests_bytes.length);
+            outputStream.write(requests_bytes, 0, requests_bytes.length);
+            log(requests.getMsgsCount() + " flag override requests sent to aconfigd");
+        } catch (IOException ioe) {
+            log("failed to send requests to aconfigd", ioe);
+            return null;
+        }
+
+        // read return
+        StorageReturnMessages return_msgs = null;
+        try {
+            int num_bytes = inputStream.readInt();
+            byte[] buffer = new byte[num_bytes];
+            inputStream.read(buffer, 0, num_bytes);
+            return_msgs = StorageReturnMessages.parseFrom(buffer);
+            log(return_msgs.getMsgsCount() + " acknowledgement received from aconfigd");
+        } catch (IOException ioe) {
+            log("failed to read requests return from aconfigd", ioe);
+            return null;
+        }
+
+        return return_msgs;
+    }
+
+    /**
+     * apply flag local override in aconfig new storage
+     * @param props
+     */
+    public static void setLocalOverridesInNewStorage(DeviceConfig.Properties props) {
+        StorageRequestMessages.Builder requests_builder = StorageRequestMessages.newBuilder();
+        for (String flagName : props.getKeyset()) {
+            String flagValue = props.getString(flagName, null);
+            if (flagName == null || flagValue == null) {
+                continue;
+            }
+
+            int idx = flagName.indexOf(":");
+            if (idx == -1 || idx == flagName.length() - 1 || idx == 0) {
+                log("invalid local flag override: " + flagName);
+                continue;
+            }
+            String actualNamespace = flagName.substring(0, idx);
+            String fullFlagName = flagName.substring(idx+1);
+            idx = fullFlagName.lastIndexOf(".");
+            if (idx == -1) {
+              log("invalid flag name: " + fullFlagName);
+              continue;
+            }
+            String packageName = fullFlagName.substring(0, idx);
+            String realFlagName = fullFlagName.substring(idx+1);
+
+            StorageRequestMessage.FlagOverrideMessage.Builder override_msg_builder =
+                StorageRequestMessage.FlagOverrideMessage.newBuilder();
+            override_msg_builder.setPackageName(packageName);
+            override_msg_builder.setFlagName(realFlagName);
+            override_msg_builder.setFlagValue(flagValue);
+            override_msg_builder.setIsLocal(true);
+
+            StorageRequestMessage.Builder request_builder = StorageRequestMessage.newBuilder();
+            request_builder.setFlagOverrideMessage(override_msg_builder.build());
+            requests_builder.addMsgs(request_builder.build());
+        }
+        StorageRequestMessages requests = requests_builder.build();
+        StorageReturnMessages acks = sendAconfigdRequests(requests);
     }
 
     public static SettingsToPropertiesMapper start(ContentResolver contentResolver) {
@@ -421,6 +546,43 @@
     }
 
     /**
+     * stage flags in aconfig new storage
+     * @param propsToStage
+     */
+    @VisibleForTesting
+    static void stageFlagsInNewStorage(HashMap<String, HashMap<String, String>> propsToStage) {
+        // create storage request proto
+        StorageRequestMessages.Builder requests_builder = StorageRequestMessages.newBuilder();
+        for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) {
+            String actualNamespace = entry.getKey();
+            HashMap<String, String> flagValuesToStage = entry.getValue();
+            for (String fullFlagName : flagValuesToStage.keySet()) {
+                String stagedValue = flagValuesToStage.get(fullFlagName);
+                int idx = fullFlagName.lastIndexOf(".");
+                if (idx == -1) {
+                    log("invalid flag name: " + fullFlagName);
+                    continue;
+                }
+                String packageName = fullFlagName.substring(0, idx);
+                String flagName = fullFlagName.substring(idx+1);
+
+                StorageRequestMessage.FlagOverrideMessage.Builder override_msg_builder =
+                    StorageRequestMessage.FlagOverrideMessage.newBuilder();
+                override_msg_builder.setPackageName(packageName);
+                override_msg_builder.setFlagName(flagName);
+                override_msg_builder.setFlagValue(stagedValue);
+                override_msg_builder.setIsLocal(false);
+
+                StorageRequestMessage.Builder request_builder = StorageRequestMessage.newBuilder();
+                request_builder.setFlagOverrideMessage(override_msg_builder.build());
+                requests_builder.addMsgs(request_builder.build());
+            }
+        }
+        StorageRequestMessages requests = requests_builder.build();
+        StorageReturnMessages acks = sendAconfigdRequests(requests);
+    }
+
+    /**
      * system property name constructing rule for aconfig flags:
      * "persist.device_config.aconfig_flags.[category_name].[flag_name]".
      * If the name contains invalid characters or substrings for system property name,
@@ -483,10 +645,10 @@
         for (String flagName : flagStagedValues.keySet()) {
           String stagedValue = flagStagedValues.get(flagName);
           String currentValue = flagCurrentValues.get(flagName);
-          if (currentValue == null) {
-            currentValue = new String("false");
+          if (stagedValue == null) {
+            continue;
           }
-          if (stagedValue != null && !stagedValue.equalsIgnoreCase(currentValue)) {
+          if (currentValue == null || !stagedValue.equalsIgnoreCase(currentValue)) {
             flagsToStage.put(flagName, stagedValue);
           }
         }
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 6308652..1db3483 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -1255,7 +1255,9 @@
             for (int uidIdx = mUidStates.size() - 1; uidIdx >= 0; uidIdx--) {
                 int uid = mUidStates.keyAt(uidIdx);
                 if (knownUids.get(uid, false)) {
-                    if (uid >= Process.FIRST_APPLICATION_UID) {
+                    int appId = UserHandle.getAppId(uid);
+                    if (appId >= Process.FIRST_APPLICATION_UID
+                            && appId <= Process.LAST_APPLICATION_UID) {
                         ArrayMap<String, Ops> pkgOps = mUidStates.valueAt(uidIdx).pkgOps;
                         for (int pkgIdx = pkgOps.size() - 1; pkgIdx >= 0; pkgIdx--) {
                             String pkgName = pkgOps.keyAt(pkgIdx);
@@ -2910,10 +2912,12 @@
         final int proxyUid = attributionSource.getUid();
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
-        final int proxiedUid = attributionSource.getNextUid();
         final int proxyVirtualDeviceId = attributionSource.getDeviceId();
+
+        final int proxiedUid = attributionSource.getNextUid();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
+        final int proxiedVirtualDeviceId = attributionSource.getNextDeviceId();
 
         verifyIncomingProxyUid(attributionSource);
         verifyIncomingOp(code);
@@ -2950,7 +2954,8 @@
 
             final SyncNotedAppOp proxyReturn = noteOperationUnchecked(code, proxyUid,
                     resolveProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
-                    Process.INVALID_UID, null, null, proxyFlags, !isProxyTrusted,
+                    Process.INVALID_UID, null, null,
+                    Context.DEVICE_ID_DEFAULT, proxyFlags, !isProxyTrusted,
                     "proxy " + message, shouldCollectMessage);
             if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) {
                 return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag,
@@ -2968,9 +2973,9 @@
         final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED
                 : AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED;
         return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
-                proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolveProxyPackageName,
-                proxyAttributionTag, proxiedFlags, shouldCollectAsyncNotedOp, message,
-                shouldCollectMessage);
+                proxiedAttributionTag, proxiedVirtualDeviceId, proxyUid, resolveProxyPackageName,
+                proxyAttributionTag, proxyVirtualDeviceId, proxiedFlags, shouldCollectAsyncNotedOp,
+                message, shouldCollectMessage);
     }
 
     @Override
@@ -3021,14 +3026,14 @@
         }
         return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
                 virtualDeviceId, Process.INVALID_UID, null, null,
-                AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp, message,
-                shouldCollectMessage);
+                Context.DEVICE_ID_DEFAULT, AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp,
+                message, shouldCollectMessage);
     }
 
     private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
             @Nullable String attributionTag, int virtualDeviceId, int proxyUid,
-            String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags,
-            boolean shouldCollectAsyncNotedOp, @Nullable String message,
+            String proxyPackageName, @Nullable String proxyAttributionTag, int proxyVirtualDeviceId,
+            @OpFlags int flags, boolean shouldCollectAsyncNotedOp, @Nullable String message,
             boolean shouldCollectMessage) {
         PackageVerificationResult pvr;
         try {
@@ -3159,8 +3164,9 @@
             }
             scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag,
                     virtualDeviceId, flags, AppOpsManager.MODE_ALLOWED);
+
             attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
-                    uidState.getState(), flags);
+                    getPersistentId(proxyVirtualDeviceId), uidState.getState(), flags);
 
             if (shouldCollectAsyncNotedOp) {
                 collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message,
@@ -3526,9 +3532,9 @@
         }
 
         return startOperationUnchecked(clientId, code, uid, packageName, attributionTag,
-                virtualDeviceId, Process.INVALID_UID, null, null, OP_FLAG_SELF,
-                startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage,
-                attributionFlags, attributionChainId);
+                virtualDeviceId, Process.INVALID_UID, null, null, Context.DEVICE_ID_DEFAULT,
+                OP_FLAG_SELF, startIfModeDefault, shouldCollectAsyncNotedOp, message,
+                shouldCollectMessage, attributionFlags, attributionChainId);
     }
 
     /** @deprecated Use {@link #startProxyOperationWithState} instead. */
@@ -3566,18 +3572,32 @@
         final int proxyUid = attributionSource.getUid();
         final String proxyPackageName = attributionSource.getPackageName();
         final String proxyAttributionTag = attributionSource.getAttributionTag();
-        final int proxiedUid = attributionSource.getNextUid();
         final int proxyVirtualDeviceId = attributionSource.getDeviceId();
+
+        final int proxiedUid = attributionSource.getNextUid();
         final String proxiedPackageName = attributionSource.getNextPackageName();
         final String proxiedAttributionTag = attributionSource.getNextAttributionTag();
+        final int proxiedVirtualDeviceId = attributionSource.getNextDeviceId();
 
         verifyIncomingProxyUid(attributionSource);
         verifyIncomingOp(code);
         if (!isValidVirtualDeviceId(proxyVirtualDeviceId)) {
-            Slog.w(TAG, "startProxyOperationImpl returned MODE_IGNORED as virtualDeviceId "
-                    + proxyVirtualDeviceId + " is invalid");
-            return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag,
-                    proxiedPackageName);
+            Slog.w(
+                    TAG,
+                    "startProxyOperationImpl returned MODE_IGNORED as proxyVirtualDeviceId "
+                            + proxyVirtualDeviceId
+                            + " is invalid");
+            return new SyncNotedAppOp(
+                    AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag, proxiedPackageName);
+        }
+        if (!isValidVirtualDeviceId(proxiedVirtualDeviceId)) {
+            Slog.w(
+                    TAG,
+                    "startProxyOperationImpl returned MODE_IGNORED as proxiedVirtualDeviceId "
+                            + proxiedVirtualDeviceId
+                            + " is invalid");
+            return new SyncNotedAppOp(
+                    AppOpsManager.MODE_IGNORED, code, proxiedAttributionTag, proxiedPackageName);
         }
         if (!isIncomingPackageValid(proxyPackageName, UserHandle.getUserId(proxyUid))
                 || !isIncomingPackageValid(proxiedPackageName, UserHandle.getUserId(proxiedUid))) {
@@ -3619,7 +3639,7 @@
             // Test if the proxied operation will succeed before starting the proxy operation
             final SyncNotedAppOp testProxiedOp = startOperationDryRun(code,
                     proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag,
-                    proxyVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
+                    proxiedVirtualDeviceId, resolvedProxyPackageName, proxiedFlags,
                     startIfModeDefault);
 
             if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) {
@@ -3631,7 +3651,7 @@
 
             final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid,
                     resolvedProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId,
-                    Process.INVALID_UID, null, null, proxyFlags,
+                    Process.INVALID_UID, null, null, Context.DEVICE_ID_DEFAULT, proxyFlags,
                     startIfModeDefault, !isProxyTrusted, "proxy " + message,
                     shouldCollectMessage, proxyAttributionFlags, attributionChainId);
             if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) {
@@ -3640,9 +3660,10 @@
         }
 
         return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
-                proxiedAttributionTag, proxyVirtualDeviceId, proxyUid, resolvedProxyPackageName,
-                proxyAttributionTag, proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp,
-                message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId);
+                proxiedAttributionTag, proxiedVirtualDeviceId, proxyUid, resolvedProxyPackageName,
+                proxyAttributionTag, proxyVirtualDeviceId, proxiedFlags, startIfModeDefault,
+                shouldCollectAsyncNotedOp, message, shouldCollectMessage, proxiedAttributionFlags,
+                attributionChainId);
     }
 
     private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
@@ -3652,9 +3673,10 @@
     private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int uid,
             @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId,
             int proxyUid, String proxyPackageName, @Nullable String proxyAttributionTag,
-            @OpFlags int flags, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp,
-            @Nullable String message, boolean shouldCollectMessage,
-            @AttributionFlags int attributionFlags, int attributionChainId) {
+            int proxyVirtualDeviceId, @OpFlags int flags, boolean startIfModeDefault,
+            boolean shouldCollectAsyncNotedOp, @Nullable String message,
+            boolean shouldCollectMessage, @AttributionFlags int attributionFlags,
+            int attributionChainId) {
         PackageVerificationResult pvr;
         try {
             pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
@@ -3749,13 +3771,13 @@
                     + " flags: " + AppOpsManager.flagsToString(flags));
             try {
                 if (isRestricted) {
-                    attributedOp.createPaused(clientId, proxyUid, proxyPackageName,
-                            proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
-                            attributionFlags, attributionChainId);
+                    attributedOp.createPaused(clientId, virtualDeviceId, proxyUid, proxyPackageName,
+                            proxyAttributionTag, getPersistentId(proxyVirtualDeviceId),
+                            uidState.getState(), flags, attributionFlags, attributionChainId);
                 } else {
-                    attributedOp.started(clientId, proxyUid, proxyPackageName,
-                            proxyAttributionTag, virtualDeviceId, uidState.getState(), flags,
-                            attributionFlags, attributionChainId);
+                    attributedOp.started(clientId, virtualDeviceId, proxyUid, proxyPackageName,
+                            proxyAttributionTag, getPersistentId(proxyVirtualDeviceId),
+                            uidState.getState(), flags, attributionFlags, attributionChainId);
                     startType = START_TYPE_STARTED;
                 }
             } catch (RemoteException e) {
@@ -4944,7 +4966,7 @@
 
         if (accessTime > 0) {
             attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg,
-                    proxyAttributionTag, uidState, opFlags);
+                    proxyAttributionTag, PERSISTENT_DEVICE_ID_DEFAULT, uidState, opFlags);
         }
         if (rejectTime > 0) {
             attributedOp.rejected(rejectTime, uidState, opFlags);
diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java
index 2285826..2760ccf 100644
--- a/services/core/java/com/android/server/appop/AttributedOp.java
+++ b/services/core/java/com/android/server/appop/AttributedOp.java
@@ -24,7 +24,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
-import android.companion.virtual.VirtualDeviceManager;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
@@ -95,16 +94,17 @@
      *
      * @param proxyUid            The uid of the proxy
      * @param proxyPackageName    The package name of the proxy
-     * @param proxyAttributionTag the attributionTag in the proxies package
+     * @param proxyAttributionTag The attributionTag in the proxies package
+     * @param proxyDeviceId       The device Id of the proxy
      * @param uidState            UID state of the app noteOp/startOp was called for
      * @param flags               OpFlags of the call
      */
     public void accessed(int proxyUid, @Nullable String proxyPackageName,
-            @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState,
-            @AppOpsManager.OpFlags int flags) {
+            @Nullable String proxyAttributionTag, @Nullable String proxyDeviceId,
+            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) {
         long accessTime = System.currentTimeMillis();
-        accessed(accessTime, -1, proxyUid, proxyPackageName,
-                proxyAttributionTag, uidState, flags);
+        accessed(accessTime, -1, proxyUid, proxyPackageName, proxyAttributionTag, proxyDeviceId,
+                uidState, flags);
 
         mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid,
                 parent.packageName, tag, uidState, flags, accessTime,
@@ -118,14 +118,16 @@
      * @param duration            The duration of the event
      * @param proxyUid            The uid of the proxy
      * @param proxyPackageName    The package name of the proxy
-     * @param proxyAttributionTag the attributionTag in the proxies package
+     * @param proxyAttributionTag The attributionTag in the proxies package
+     * @param proxyDeviceId       The device Id of the proxy
      * @param uidState            UID state of the app noteOp/startOp was called for
      * @param flags               OpFlags of the call
      */
     @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
     public void accessed(long noteTime, long duration, int proxyUid,
             @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) {
+            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
+            @AppOpsManager.OpFlags int flags) {
         long key = makeKey(uidState, flags);
 
         if (mAccessEvents == null) {
@@ -135,7 +137,7 @@
         AppOpsManager.OpEventProxyInfo proxyInfo = null;
         if (proxyUid != Process.INVALID_UID) {
             proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName,
-                            proxyAttributionTag, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
+                    proxyAttributionTag, proxyDeviceId);
         }
 
         AppOpsManager.NoteOpEvent existingEvent = mAccessEvents.get(key);
@@ -189,35 +191,36 @@
      * Update state when start was called
      *
      * @param clientId            Id of the startOp caller
+     * @param virtualDeviceId     The virtual device id of the startOp caller
      * @param proxyUid            The UID of the proxy app
      * @param proxyPackageName    The package name of the proxy app
      * @param proxyAttributionTag The attribution tag of the proxy app
+     * @param proxyDeviceId       The device id of the proxy app
      * @param uidState            UID state of the app startOp is called for
      * @param flags               The proxy flags
      * @param attributionFlags    The attribution flags associated with this operation.
-     * @param attributionChainId  The if of the attribution chain this operations is a part of.
+     * @param attributionChainId  The if of the attribution chain this operations is a part of
      */
-    public void started(@NonNull IBinder clientId, int proxyUid,
+    public void started(@NonNull IBinder clientId, int virtualDeviceId, int proxyUid,
             @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
+            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
             @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
             int attributionChainId) throws RemoteException {
-        startedOrPaused(clientId, proxyUid, proxyPackageName,
-                proxyAttributionTag, proxyVirtualDeviceId, uidState, flags,
-                /* triggeredByUidStateChange */ false, /* isStarted */ true, attributionFlags,
-                attributionChainId);
+        startedOrPaused(clientId, virtualDeviceId, proxyUid, proxyPackageName, proxyAttributionTag,
+                proxyDeviceId, uidState, flags, attributionFlags, attributionChainId, false,
+                true);
     }
 
     @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService
-    private void startedOrPaused(@NonNull IBinder clientId, int proxyUid,
+    private void startedOrPaused(@NonNull IBinder clientId, int virtualDeviceId, int proxyUid,
             @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
-            @AppOpsManager.OpFlags int flags, boolean triggeredByUidStateChange,
-            boolean isStarted, @AppOpsManager.AttributionFlags int attributionFlags,
-            int attributionChainId) throws RemoteException {
+            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
+            @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
+            int attributionChainId, boolean triggeredByUidStateChange, boolean isStarted)
+            throws RemoteException {
         if (!triggeredByUidStateChange && !parent.isRunning() && isStarted) {
             mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid,
-                    parent.packageName, tag, proxyVirtualDeviceId, true, attributionFlags,
+                    parent.packageName, tag, virtualDeviceId, true, attributionFlags,
                     attributionChainId);
         }
 
@@ -233,9 +236,9 @@
         InProgressStartOpEvent event = events.get(clientId);
         if (event == null) {
             event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime,
-                    SystemClock.elapsedRealtime(), clientId, tag, proxyVirtualDeviceId,
+                    SystemClock.elapsedRealtime(), clientId, tag, virtualDeviceId,
                     PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId),
-                    proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags,
+                    proxyUid, proxyPackageName, proxyAttributionTag, proxyDeviceId, uidState, flags,
                     attributionFlags, attributionChainId);
             events.put(clientId, event);
         } else {
@@ -366,15 +369,14 @@
     /**
      * Create an event that will be started, if the op is unpaused.
      */
-    public void createPaused(@NonNull IBinder clientId, int proxyUid,
-            @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
-            int proxyVirtualDeviceId, @AppOpsManager.UidState int uidState,
-            @AppOpsManager.OpFlags int flags,
-            @AppOpsManager.AttributionFlags int attributionFlags,
+    public void createPaused(@NonNull IBinder clientId, int virtualDeviceId,
+            int proxyUid, @Nullable String proxyPackageName, @Nullable String proxyAttributionTag,
+            @Nullable String proxyDeviceId, @AppOpsManager.UidState int uidState,
+            @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags int attributionFlags,
             int attributionChainId) throws RemoteException {
-        startedOrPaused(clientId, proxyUid, proxyPackageName, proxyAttributionTag,
-                proxyVirtualDeviceId, uidState, flags, false, false,
-                attributionFlags, attributionChainId);
+        startedOrPaused(clientId, virtualDeviceId, proxyUid, proxyPackageName, proxyAttributionTag,
+                proxyDeviceId, uidState, flags, attributionFlags, attributionChainId, false,
+                false);
     }
 
     /**
@@ -496,16 +498,16 @@
                     // Call started() to add a new start event object and then add the
                     // previously removed unfinished start counts back
                     if (proxy != null) {
-                        startedOrPaused(event.getClientId(), proxy.getUid(),
-                                proxy.getPackageName(), proxy.getAttributionTag(),
-                                event.getVirtualDeviceId(), newState, event.getFlags(),
-                                true, isRunning,
-                                event.getAttributionFlags(), event.getAttributionChainId());
+                        startedOrPaused(event.getClientId(), event.getVirtualDeviceId(),
+                                proxy.getUid(), proxy.getPackageName(), proxy.getAttributionTag(),
+                                proxy.getDeviceId(), newState, event.getFlags(),
+                                event.getAttributionFlags(), event.getAttributionChainId(), true,
+                                isRunning);
                     } else {
-                        startedOrPaused(event.getClientId(), Process.INVALID_UID, null, null,
-                                event.getVirtualDeviceId(), newState, event.getFlags(), true,
-                                isRunning, event.getAttributionFlags(),
-                                event.getAttributionChainId());
+                        startedOrPaused(event.getClientId(), event.getVirtualDeviceId(),
+                                Process.INVALID_UID, null, null, null,
+                                newState, event.getFlags(), event.getAttributionFlags(),
+                                event.getAttributionChainId(), true, isRunning);
                     }
 
                     events = isRunning ? mInProgressEvents : mPausedInProgressEvents;
@@ -847,7 +849,8 @@
         InProgressStartOpEvent acquire(long startTime, long elapsedTime, @NonNull IBinder clientId,
                 @Nullable String attributionTag, int virtualDeviceId,  @NonNull Runnable onDeath,
                 int proxyUid, @Nullable String proxyPackageName,
-                @Nullable String proxyAttributionTag, @AppOpsManager.UidState int uidState,
+                @Nullable String proxyAttributionTag, @Nullable String proxyDeviceId,
+                @AppOpsManager.UidState int uidState,
                 @AppOpsManager.OpFlags int flags, @AppOpsManager.AttributionFlags
                 int attributionFlags, int attributionChainId) throws RemoteException {
 
@@ -856,7 +859,7 @@
             AppOpsManager.OpEventProxyInfo proxyInfo = null;
             if (proxyUid != Process.INVALID_UID) {
                 proxyInfo = mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName,
-                        proxyAttributionTag, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
+                        proxyAttributionTag, proxyDeviceId);
             }
 
             if (recycled != null) {
@@ -880,7 +883,8 @@
             super(maxUnusedPooledObjects);
         }
 
-        AppOpsManager.OpEventProxyInfo acquire(@IntRange(from = 0) int uid,
+        AppOpsManager.OpEventProxyInfo acquire(
+                @IntRange(from = 0) int uid,
                 @Nullable String packageName,
                 @Nullable String attributionTag,
                 @Nullable String deviceId) {
@@ -890,7 +894,7 @@
                 return recycled;
             }
 
-            return new AppOpsManager.OpEventProxyInfo(uid, packageName, attributionTag);
+            return new AppOpsManager.OpEventProxyInfo(uid, packageName, attributionTag, deviceId);
         }
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java
new file mode 100644
index 0000000..7cf2d30
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java
@@ -0,0 +1,95 @@
+/*
+ * 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.biometrics;
+
+import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
+import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.biometrics.BiometricsProtoEnums;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.server.biometrics.sensors.BiometricNotificationUtils;
+
+/**
+ * Receives broadcast to biometrics dangling notification.
+ */
+public class BiometricDanglingReceiver extends BroadcastReceiver {
+    private static final String TAG = "BiometricDanglingReceiver";
+
+    public static final String ACTION_FINGERPRINT_RE_ENROLL_LAUNCH =
+            "action_fingerprint_re_enroll_launch";
+    public static final String ACTION_FINGERPRINT_RE_ENROLL_DISMISS =
+            "action_fingerprint_re_enroll_dismiss";
+
+    public static final String ACTION_FACE_RE_ENROLL_LAUNCH =
+            "action_face_re_enroll_launch";
+    public static final String ACTION_FACE_RE_ENROLL_DISMISS =
+            "action_face_re_enroll_dismiss";
+
+    public static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+
+    /**
+     * Constructor for BiometricDanglingReceiver.
+     *
+     * @param context context
+     * @param modality the value from BiometricsProtoEnums.MODALITY_*
+     */
+    public BiometricDanglingReceiver(@NonNull Context context, int modality) {
+        final IntentFilter intentFilter = new IntentFilter();
+        if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
+            intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+            intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_DISMISS);
+        } else if (modality == BiometricsProtoEnums.MODALITY_FACE) {
+            intentFilter.addAction(ACTION_FACE_RE_ENROLL_LAUNCH);
+            intentFilter.addAction(ACTION_FACE_RE_ENROLL_DISMISS);
+        }
+        context.registerReceiver(this, intentFilter, Context.RECEIVER_NOT_EXPORTED);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Slog.d(TAG, "Received: " + intent.getAction());
+        if (ACTION_FINGERPRINT_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
+            launchBiometricEnrollActivity(context, Settings.ACTION_FINGERPRINT_ENROLL);
+            BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
+        } else if (ACTION_FINGERPRINT_RE_ENROLL_DISMISS.equals(intent.getAction())) {
+            BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
+        } else if (ACTION_FACE_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
+            launchBiometricEnrollActivity(context, FACE_SETTINGS_ACTION);
+            BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
+        } else if (ACTION_FACE_RE_ENROLL_DISMISS.equals(intent.getAction())) {
+            BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
+        }
+        context.unregisterReceiver(this);
+    }
+
+    private void launchBiometricEnrollActivity(Context context, String action) {
+        context.sendBroadcast(
+                new Intent(ACTION_CLOSE_SYSTEM_DIALOGS).setFlags(FLAG_RECEIVER_FOREGROUND));
+        final Intent intent = new Intent(action);
+        intent.setPackage(SETTINGS_PACKAGE);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 0e22f75..eaa5e2a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -24,13 +24,18 @@
 import android.content.Context;
 import android.content.Intent;
 import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.face.FaceEnrollOptions;
 import android.hardware.fingerprint.FingerprintEnrollOptions;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.text.BidiFormatter;
 import android.util.Slog;
 
 import com.android.internal.R;
+import com.android.server.biometrics.BiometricDanglingReceiver;
+
+import java.util.List;
 
 /**
  * Biometric notification helper class.
@@ -39,6 +44,7 @@
 
     private static final String TAG = "BiometricNotificationUtils";
     private static final String FACE_RE_ENROLL_NOTIFICATION_TAG = "FaceReEnroll";
+    private static final String FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG = "FingerprintReEnroll";
     private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintBadCalibration";
     private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock";
     private static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
@@ -50,6 +56,8 @@
     private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel";
     private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel";
     private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel";
+    private static final String FINGERPRINT_RE_ENROLL_CHANNEL =
+            "FingerprintReEnrollNotificationChannel";
     private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL =
             "FingerprintBadCalibrationNotificationChannel";
     private static final long NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -177,10 +185,124 @@
                 BAD_CALIBRATION_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false);
     }
 
+    /**
+     * Shows a biometric re-enroll notification.
+     */
+    public static void showBiometricReEnrollNotification(@NonNull Context context,
+            @NonNull List<String> identifiers, boolean allIdentifiersDeleted, int modality) {
+        final boolean isFingerprint = modality == BiometricsProtoEnums.MODALITY_FINGERPRINT;
+        final String reEnrollName = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
+                : FACE_RE_ENROLL_NOTIFICATION_TAG;
+        if (identifiers.isEmpty()) {
+            Slog.v(TAG, "Skipping " + reEnrollName + " notification : empty list");
+            return;
+        }
+        Slog.d(TAG, "Showing " + reEnrollName + " notification :[" + identifiers.size()
+                + " identifier(s) deleted, allIdentifiersDeleted=" + allIdentifiersDeleted + "]");
+
+        final String name =
+                context.getString(R.string.device_unlock_notification_name);
+        final String title = context.getString(isFingerprint
+                ? R.string.fingerprint_dangling_notification_title
+                : R.string.face_dangling_notification_title);
+        final String content = isFingerprint
+                ? getFingerprintDanglingContentString(context, identifiers, allIdentifiersDeleted)
+                : context.getString(R.string.face_dangling_notification_msg);
+
+        // Create "Set up" notification action button.
+        final Intent setupIntent = new Intent(
+                isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH
+                : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
+        final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+                setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+        final String setupText =
+                context.getString(R.string.biometric_dangling_notification_action_set_up);
+        final Notification.Action setupAction = new Notification.Action.Builder(
+                null, setupText, setupPendingIntent).build();
+
+        // Create "Not now" notification action button.
+        final Intent notNowIntent = new Intent(
+                isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS
+                : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_DISMISS);
+        final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+                notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+        final String notNowText = context.getString(
+                R.string.biometric_dangling_notification_action_not_now);
+        final Notification.Action notNowAction = new Notification.Action.Builder(
+                null, notNowText, notNowPendingIntent).build();
+
+        final String channel = isFingerprint ? FINGERPRINT_RE_ENROLL_CHANNEL
+                : FACE_RE_ENROLL_CHANNEL;
+        final String tag = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
+                : FACE_RE_ENROLL_NOTIFICATION_TAG;
+
+        showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction,
+                notNowAction, Notification.CATEGORY_SYSTEM, channel, tag,
+                Notification.VISIBILITY_SECRET, false);
+    }
+
+    private static String getFingerprintDanglingContentString(Context context,
+            @NonNull List<String> fingerprints, boolean allFingerprintDeleted) {
+        if (fingerprints.isEmpty()) {
+            return null;
+        }
+
+        final int resId;
+        final int size = fingerprints.size();
+        final StringBuilder first = new StringBuilder();
+        final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+        if (size > 1) {
+            // If there are more than 1 fingerprint deleted, the "second" will be the last
+            // fingerprint and set the others to "first".
+            // For example, if we have 3 fingerprints deleted(fp1, fp2 and fp3):
+            //   first  = "fp1, fp2"
+            //   second = "fp3"
+            final String separator = ", ";
+            String second = null;
+            for (int i = 0; i < size; i++) {
+                if (i == size - 1) {
+                    second = bidiFormatter.unicodeWrap("\"" + fingerprints.get(i) + "\"");
+                } else {
+                    first.append(bidiFormatter.unicodeWrap("\""));
+                    first.append(bidiFormatter.unicodeWrap(fingerprints.get(i)));
+                    first.append(bidiFormatter.unicodeWrap("\""));
+                    if (i < size - 2) {
+                        first.append(bidiFormatter.unicodeWrap(separator));
+                    }
+                }
+            }
+            if (allFingerprintDeleted) {
+                resId = R.string.fingerprint_dangling_notification_msg_all_deleted_2;
+            } else {
+                resId = R.string.fingerprint_dangling_notification_msg_2;
+            }
+
+            return String.format(context.getString(resId), first, second);
+        } else {
+            if (allFingerprintDeleted) {
+                resId = R.string.fingerprint_dangling_notification_msg_all_deleted_1;
+            } else {
+                resId = R.string.fingerprint_dangling_notification_msg_1;
+            }
+            first.append(bidiFormatter.unicodeWrap("\""));
+            first.append(bidiFormatter.unicodeWrap(fingerprints.get(0)));
+            first.append(bidiFormatter.unicodeWrap("\""));
+            return String.format(context.getString(resId), first);
+        }
+    }
+
     private static void showNotificationHelper(Context context, String name, String title,
-                String content, PendingIntent pendingIntent, String category,
-                String channelName, String notificationTag, int visibility,
-                boolean listenToDismissEvent) {
+            String content, PendingIntent pendingIntent, String category, String channelName,
+            String notificationTag, int visibility, boolean listenToDismissEvent) {
+        showNotificationHelper(context, name, title, content, pendingIntent,
+                null /* positiveAction */, null /* negativeAction */, category, channelName,
+                notificationTag, visibility, listenToDismissEvent);
+    }
+
+    private static void showNotificationHelper(Context context, String name, String title,
+            String content, PendingIntent pendingIntent, Notification.Action positiveAction,
+            Notification.Action negativeAction, String category, String channelName,
+            String notificationTag, int visibility, boolean listenToDismissEvent) {
         Slog.v(TAG," listenToDismissEvent = " + listenToDismissEvent);
         final PendingIntent dismissIntent = PendingIntent.getActivityAsUser(context,
                 0 /* requestCode */, DISMISS_FRR_INTENT, PendingIntent.FLAG_IMMUTABLE /* flags */,
@@ -202,6 +324,12 @@
                 .setContentIntent(pendingIntent)
                 .setVisibility(visibility);
 
+        if (positiveAction != null) {
+            builder.addAction(positiveAction);
+        }
+        if (negativeAction != null) {
+            builder.addAction(negativeAction);
+        }
         if (listenToDismissEvent) {
             builder.setDeleteIntent(dismissIntent);
         }
@@ -253,4 +381,14 @@
                 UserHandle.CURRENT);
     }
 
+    /**
+     * Cancels a fingerprint enrollment notification
+     */
+    public static void cancelFingerprintReEnrollNotification(@NonNull Context context) {
+        final NotificationManager notificationManager =
+                context.getSystemService(NotificationManager.class);
+        notificationManager.cancelAsUser(FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID,
+                UserHandle.CURRENT);
+    }
+
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
index 6daaad1..81ab26d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
@@ -22,6 +22,7 @@
 import android.os.IBinder;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.BiometricsProto;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
@@ -44,6 +45,7 @@
     private List<? extends BiometricAuthenticator.Identifier> mEnrolledList;
     // List of templates to remove from the HAL
     private List<BiometricAuthenticator.Identifier> mUnknownHALTemplates = new ArrayList<>();
+    private final int mInitialEnrolledSize;
 
     protected InternalEnumerateClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
             @NonNull IBinder token, int userId, @NonNull String owner,
@@ -55,6 +57,7 @@
         super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner,
                 0 /* cookie */, sensorId, logger, biometricContext);
         mEnrolledList = enrolledList;
+        mInitialEnrolledSize = mEnrolledList.size();
         mUtils = utils;
     }
 
@@ -111,8 +114,10 @@
 
         // At this point, mEnrolledList only contains templates known to the framework and
         // not the HAL.
+        final List<String> names = new ArrayList<>();
         for (int i = 0; i < mEnrolledList.size(); i++) {
             BiometricAuthenticator.Identifier identifier = mEnrolledList.get(i);
+            names.add(identifier.getName().toString());
             Slog.e(TAG, "doTemplateCleanup(): Removing dangling template from framework: "
                     + identifier.getBiometricId() + " " + identifier.getName());
             mUtils.removeBiometricForUser(getContext(),
@@ -120,6 +125,11 @@
 
             getLogger().logUnknownEnrollmentInFramework();
         }
+
+        // Send dangling notification.
+        if (!names.isEmpty()) {
+            sendDanglingNotification(names);
+        }
         mEnrolledList.clear();
     }
 
@@ -127,8 +137,24 @@
         return mUnknownHALTemplates;
     }
 
+    /**
+     * Send the dangling notification.
+     */
+    @VisibleForTesting
+    public void sendDanglingNotification(@NonNull List<String> identifierNames) {
+        if (!identifierNames.isEmpty()) {
+            Slog.e(TAG, "sendDanglingNotification(): initial enrolledSize="
+                    + mInitialEnrolledSize + ", after clean up size=" + mEnrolledList.size());
+            final boolean allIdentifiersDeleted = mEnrolledList.size() == mInitialEnrolledSize;
+            BiometricNotificationUtils.showBiometricReEnrollNotification(
+                    getContext(), identifierNames, allIdentifiersDeleted, getModality());
+        }
+    }
+
     @Override
     public int getProtoEnum() {
         return BiometricsProto.CM_ENUMERATE;
     }
+
+    protected abstract int getModality();
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
index d85455e..6ce3bc5 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
@@ -18,12 +18,14 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.face.IFace;
 import android.hardware.face.Face;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.BiometricUtils;
@@ -35,7 +37,8 @@
 /**
  * Face-specific internal enumerate client for the {@link IFace} AIDL HAL interface.
  */
-class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
+@VisibleForTesting
+public class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
     private static final String TAG = "FaceInternalEnumerateClient";
 
     FaceInternalEnumerateClient(@NonNull Context context,
@@ -56,4 +59,9 @@
             mCallback.onClientFinished(this, false /* success */);
         }
     }
+
+    @Override
+    protected int getModality() {
+        return BiometricsProtoEnums.MODALITY_FACE;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index e71cffe..f0a4189 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,6 +52,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
 import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricDanglingReceiver;
 import com.android.server.biometrics.BiometricHandlerProvider;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
@@ -201,6 +202,7 @@
         mBiometricHandlerProvider = biometricHandlerProvider;
 
         initAuthenticationBroadcastReceiver();
+        initFaceDanglingBroadcastReceiver();
         initSensors(resetLockoutRequiresChallenge, props);
     }
 
@@ -214,6 +216,10 @@
                 });
     }
 
+    private void initFaceDanglingBroadcastReceiver() {
+        new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FACE);
+    }
+
     private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) {
         if (resetLockoutRequiresChallenge) {
             Slog.d(getTag(), "Adding HIDL configs");
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
index a5a832a..2849bd9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
@@ -18,11 +18,13 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.fingerprint.Fingerprint;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.log.BiometricContext;
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.BiometricUtils;
@@ -35,7 +37,8 @@
  * Fingerprint-specific internal client supporting the
  * {@link android.hardware.biometrics.fingerprint.IFingerprint} AIDL interface.
  */
-class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
+@VisibleForTesting
+public class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
     private static final String TAG = "FingerprintInternalEnumerateClient";
 
     protected FingerprintInternalEnumerateClient(@NonNull Context context,
@@ -56,4 +59,9 @@
             mCallback.onClientFinished(this, false /* success */);
         }
     }
+
+    @Override
+    protected int getModality() {
+        return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 6874c71..c0dcd49 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -58,6 +58,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
 import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricDanglingReceiver;
 import com.android.server.biometrics.BiometricHandlerProvider;
 import com.android.server.biometrics.Flags;
 import com.android.server.biometrics.Utils;
@@ -205,6 +206,7 @@
         mBiometricHandlerProvider = biometricHandlerProvider;
 
         initAuthenticationBroadcastReceiver();
+        initFingerprintDanglingBroadcastReceiver();
         initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
     }
 
@@ -218,6 +220,10 @@
                 });
     }
 
+    private void initFingerprintDanglingBroadcastReceiver() {
+        new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+    }
+
     private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props,
             GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
         if (!resetLockoutRequiresHardwareAuthToken) {
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 645a366..390ee96 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -37,6 +37,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
 import android.content.res.Configuration;
+import android.graphics.ImageFormat;
 import android.graphics.Rect;
 import android.hardware.CameraExtensionSessionStats;
 import android.hardware.CameraSessionStats;
@@ -906,6 +907,7 @@
 
             int extensionType = FrameworkStatsLog.CAMERA_ACTION_EVENT__EXT_TYPE__EXTENSION_NONE;
             boolean extensionIsAdvanced = false;
+            int extensionCaptureFormat = ImageFormat.UNKNOWN;
             if (e.mExtSessionStats != null) {
                 switch (e.mExtSessionStats.type) {
                     case CameraExtensionSessionStats.Type.EXTENSION_AUTOMATIC:
@@ -932,6 +934,9 @@
                         Slog.w(TAG, "Unknown extension type: " + e.mExtSessionStats.type);
                 }
                 extensionIsAdvanced = e.mExtSessionStats.isAdvanced;
+                if (Flags.analytics24q3()) {
+                    extensionCaptureFormat = e.mExtSessionStats.captureFormat;
+                }
             }
 
             int streamCount = 0;
@@ -945,10 +950,13 @@
                 String zoomOverrideDebug = Flags.logZoomOverrideUsage()
                         ? ", zoomOverrideUsage " + e.mUsedZoomOverride
                         : "";
-
                 String mostRequestedFpsRangeDebug = Flags.analytics24q3()
                         ? ", mostRequestedFpsRange " + e.mMostRequestedFpsRange
                         : "";
+                String extensionCaptureFormatDebug = Flags.analytics24q3()
+                        ? " extensionCaptureFormat " + e.mExtSessionStats.captureFormat
+                        : "";
+
                 Slog.v(TAG, "CAMERA_ACTION_EVENT: action " + e.mAction
                         + " clientName " + e.mClientName
                         + ", duration " + e.getDuration()
@@ -971,8 +979,10 @@
                         + ", logId " + e.mLogId
                         + ", sessionIndex " + e.mSessionIndex
                         + ", mExtSessionStats {type " + extensionType
-                        + " isAdvanced " + extensionIsAdvanced + "}");
+                        + " isAdvanced " + extensionIsAdvanced
+                        + extensionCaptureFormatDebug + "}");
             }
+
             // Convert from CameraStreamStats to CameraStreamProto
             CameraStreamProto[] streamProtos = new CameraStreamProto[MAX_STREAM_STATISTICS];
             for (int i = 0; i < MAX_STREAM_STATISTICS; i++) {
@@ -1035,7 +1045,8 @@
                     e.mLogId, e.mSessionIndex,
                     extensionType, extensionIsAdvanced, e.mUsedUltraWide,
                     e.mUsedZoomOverride,
-                    e.mMostRequestedFpsRange.getLower(), e.mMostRequestedFpsRange.getUpper());
+                    e.mMostRequestedFpsRange.getLower(), e.mMostRequestedFpsRange.getUpper(),
+                    extensionCaptureFormat);
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index 22f3bbd..fa8299b 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -29,7 +29,7 @@
  */
 public class DisplayControl {
     private static native IBinder nativeCreateDisplay(String name, boolean secure,
-            float requestedRefreshRate);
+            String uniqueId, float requestedRefreshRate);
     private static native void nativeDestroyDisplay(IBinder displayToken);
     private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
     private static native long[] nativeGetPhysicalDisplayIds();
@@ -43,20 +43,21 @@
     /**
      * Create a display in SurfaceFlinger.
      *
-     * @param name The name of the display
+     * @param name The name of the display.
      * @param secure Whether this display is secure.
      * @return The token reference for the display in SurfaceFlinger.
      */
     public static IBinder createDisplay(String name, boolean secure) {
         Objects.requireNonNull(name, "name must not be null");
-        return nativeCreateDisplay(name, secure, 0.0f);
+        return nativeCreateDisplay(name, secure, "", 0.0f);
     }
 
     /**
      * Create a display in SurfaceFlinger.
      *
-     * @param name The name of the display
+     * @param name The name of the display.
      * @param secure Whether this display is secure.
+     * @param uniqueId The unique ID for the display.
      * @param requestedRefreshRate The requested refresh rate in frames per second.
      * For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
      * 120hz display. If an arbitrary refresh rate is specified, the rate will be rounded
@@ -65,9 +66,10 @@
      * @return The token reference for the display in SurfaceFlinger.
      */
     public static IBinder createDisplay(String name, boolean secure,
-            float requestedRefreshRate) {
+            String uniqueId, float requestedRefreshRate) {
         Objects.requireNonNull(name, "name must not be null");
-        return nativeCreateDisplay(name, secure, requestedRefreshRate);
+        Objects.requireNonNull(uniqueId, "uniqueId must not be null");
+        return nativeCreateDisplay(name, secure, uniqueId, requestedRefreshRate);
     }
 
     /**
@@ -79,7 +81,6 @@
         if (displayToken == null) {
             throw new IllegalArgumentException("displayToken must not be null");
         }
-
         nativeDestroyDisplay(displayToken);
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d7a7dd4..70a1014 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1442,6 +1442,9 @@
 
         // If there's an offload session, we need to set the initial doze brightness before
         // the offload session starts controlling the brightness.
+        // During the transition DOZE_SUSPEND -> DOZE -> DOZE_SUSPEND, this brightness strategy
+        // will be selected again, meaning that no new brightness will be sent to the hardware and
+        // the display will stay at the brightness level set by the offload session.
         if (Float.isNaN(brightnessState) && mFlags.isDisplayOffloadEnabled()
                 && Display.isDozeState(state) && mDisplayOffloadSession != null) {
             if (mAutomaticBrightnessController != null
@@ -1459,6 +1462,15 @@
             if (BrightnessUtils.isValidBrightnessValue(rawBrightnessState)) {
                 brightnessState = clampScreenBrightness(rawBrightnessState);
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_INITIAL);
+
+                if (mAutomaticBrightnessController != null
+                        && mAutomaticBrightnessStrategy.shouldUseAutoBrightness()) {
+                    // Keep the brightness in the setting so that we can use it after the screen
+                    // turns on, until a lux sample becomes available. We don't do this when
+                    // auto-brightness is disabled - in that situation we still want to use
+                    // the last brightness from when the screen was on.
+                    updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index bcdb442..a29e852 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -92,8 +92,9 @@
             Context context, Handler handler, Listener listener, DisplayManagerFlags featureFlags) {
         this(syncRoot, context, handler, listener, new SurfaceControlDisplayFactory() {
             @Override
-            public IBinder createDisplay(String name, boolean secure, float requestedRefreshRate) {
-                return DisplayControl.createDisplay(name, secure, requestedRefreshRate);
+            public IBinder createDisplay(String name, boolean secure, String uniqueId,
+                                         float requestedRefreshRate) {
+                return DisplayControl.createDisplay(name, secure, uniqueId, requestedRefreshRate);
             }
 
             @Override
@@ -126,7 +127,7 @@
         String name = virtualDisplayConfig.getName();
         boolean secure = (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0;
 
-        IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure,
+        IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, uniqueId,
                 virtualDisplayConfig.getRequestedRefreshRate());
         MediaProjectionCallback mediaProjectionCallback =  null;
         if (projection != null) {
@@ -653,8 +654,9 @@
         /**
          * Create a virtual display in SurfaceFlinger.
          *
-         * @param name The name of the display
+         * @param name The name of the display.
          * @param secure Whether this display is secure.
+         * @param uniqueId The unique ID for the display.
          * @param requestedRefreshRate
          *     The refresh rate, frames per second, to request on the virtual display.
          *     It should be a divisor of refresh rate of the leader physical display
@@ -663,8 +665,9 @@
          *     the refresh rate of the leader physical display.
          * @return The token reference for the display in SurfaceFlinger.
          */
-        IBinder createDisplay(String name, boolean secure, float requestedRefreshRate);
-        
+        IBinder createDisplay(String name, boolean secure, String uniqueId,
+                              float requestedRefreshRate);
+
         /**
          * Destroy a display in SurfaceFlinger.
          *
diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java
index 2703a2c0..7e18d84 100644
--- a/services/core/java/com/android/server/hdmi/SendKeyAction.java
+++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java
@@ -158,9 +158,11 @@
                 mTargetAddress, cecKeycodeAndParams), new SendMessageCallback() {
                 @Override
                 public void onSendCompleted(int error) {
-                    if (error != SendMessageResult.SUCCESS) {
+                    // Disable System Audio Mode, if the AVR doesn't acknowledge
+                    // a <User Control Pressed> message.
+                    if (error == SendMessageResult.NACK) {
                         HdmiLogger.debug(
-                            "AVR did not respond to <User Control Pressed>");
+                            "AVR did not acknowledge <User Control Pressed>");
                         localDevice().mService.setSystemAudioActivated(false);
                     }
                 }
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 4e9cf51..b47631c3 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -84,29 +84,6 @@
             @NonNull IBinder toChannelToken);
 
     /**
-     * Sets the display id that the MouseCursorController will be forced to target. Pass
-     * {@link android.view.Display#INVALID_DISPLAY} to clear the override.
-     *
-     * Note: This method generally blocks until the pointer display override has propagated.
-     * When setting a new override, the caller should ensure that an input device that can control
-     * the mouse pointer is connected. If a new override is set when no such input device is
-     * connected, the caller may be blocked for an arbitrary period of time.
-     *
-     * @return true if the pointer displayId was set successfully, or false if it fails.
-     *
-     * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete.
-     */
-    public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId);
-
-    /**
-     * Gets the display id that the MouseCursorController is being forced to target. Returns
-     * {@link android.view.Display#INVALID_DISPLAY} if there is no override
-     *
-     * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete.
-     */
-    public abstract int getVirtualMousePointerDisplayId();
-
-    /**
      * Gets the current position of the mouse cursor.
      *
      * Returns NaN-s as the coordinates if the cursor is not available.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index eb71952..8317991 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -156,7 +156,6 @@
     private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1;
     private static final int MSG_RELOAD_DEVICE_ALIASES = 2;
     private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 3;
-    private static final int MSG_POINTER_DISPLAY_ID_CHANGED = 4;
 
     private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
     private static final AdditionalDisplayInputProperties
@@ -255,7 +254,7 @@
     // to {DisplayInfo#uniqueId} (String) so that events from the Input Device go to a
     // specific display.
     @GuardedBy("mAssociationsLock")
-    private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>();
+    private final Map<String, String> mUniqueIdAssociationsByPort = new ArrayMap<>();
 
     // The associations of input devices to displays by descriptor. Maps from
     // {InputDevice#mDescriptor} to {DisplayInfo#uniqueId} (String) so that events from the
@@ -282,33 +281,9 @@
     // WARNING: Do not call other services outside of input while holding this lock.
     private final Object mAdditionalDisplayInputPropertiesLock = new Object();
 
-    // Forces the PointerController to target a specific display id.
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private int mOverriddenPointerDisplayId = Display.INVALID_DISPLAY;
-
-    // PointerController is the source of truth of the pointer display. This is the value of the
-    // latest pointer display id reported by PointerController.
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private int mAcknowledgedPointerDisplayId = Display.INVALID_DISPLAY;
-    // This is the latest display id that IMS has requested PointerController to use. If there are
-    // no devices that can control the pointer, PointerController may end up disregarding this
-    // value.
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private int mRequestedPointerDisplayId = Display.INVALID_DISPLAY;
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
     private final SparseArray<AdditionalDisplayInputProperties> mAdditionalDisplayInputProperties =
             new SparseArray<>();
-    // This contains the per-display properties that are currently applied by native code. It should
-    // be kept in sync with the properties for mRequestedPointerDisplayId.
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private final AdditionalDisplayInputProperties mCurrentDisplayProperties =
-            new AdditionalDisplayInputProperties();
-    // TODO(b/293587049): Pointer Icon Refactor: There can be more than one pointer icon
-    // visible at once. Update this to support multi-pointer use cases.
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private PointerIcon mPointerIcon;
 
     // Holds all the registered gesture monitors that are implemented as spy windows. The spy
     // windows are mapped by their InputChannel tokens.
@@ -617,14 +592,9 @@
         }
         mNative.setDisplayViewports(vArray);
 
-        // Attempt to update the pointer display when viewports change when there is no override.
+        // Attempt to update the default pointer display when the viewports change.
         // Take care to not make calls to window manager while holding internal locks.
-        final int pointerDisplayId = mWindowManagerCallbacks.getPointerDisplayId();
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            if (mOverriddenPointerDisplayId == Display.INVALID_DISPLAY) {
-                updatePointerDisplayIdLocked(pointerDisplayId);
-            }
-        }
+        mNative.setPointerDisplayId(mWindowManagerCallbacks.getPointerDisplayId());
     }
 
     /**
@@ -1353,84 +1323,6 @@
                 properties -> properties.pointerIconVisible = visible);
     }
 
-    /**
-     * Update the display on which the mouse pointer is shown.
-     *
-     * @return true if the pointer displayId changed, false otherwise.
-     */
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private boolean updatePointerDisplayIdLocked(int pointerDisplayId) {
-        if (mRequestedPointerDisplayId == pointerDisplayId) {
-            return false;
-        }
-        mRequestedPointerDisplayId = pointerDisplayId;
-        mNative.setPointerDisplayId(pointerDisplayId);
-        applyAdditionalDisplayInputProperties();
-        return true;
-    }
-
-    private void handlePointerDisplayIdChanged(PointerDisplayIdChangedArgs args) {
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mAcknowledgedPointerDisplayId = args.mPointerDisplayId;
-            // Notify waiting threads that the display of the mouse pointer has changed.
-            mAdditionalDisplayInputPropertiesLock.notifyAll();
-        }
-        mWindowManagerCallbacks.notifyPointerDisplayIdChanged(
-                args.mPointerDisplayId, args.mXPosition, args.mYPosition);
-    }
-
-    private boolean setVirtualMousePointerDisplayIdBlocking(int overrideDisplayId) {
-        if (com.android.input.flags.Flags.enablePointerChoreographer()) {
-            throw new IllegalStateException(
-                    "This must not be used when PointerChoreographer is enabled");
-        }
-        final boolean isRemovingOverride = overrideDisplayId == Display.INVALID_DISPLAY;
-
-        // Take care to not make calls to window manager while holding internal locks.
-        final int resolvedDisplayId = isRemovingOverride
-                ? mWindowManagerCallbacks.getPointerDisplayId()
-                : overrideDisplayId;
-
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mOverriddenPointerDisplayId = overrideDisplayId;
-
-            if (!updatePointerDisplayIdLocked(resolvedDisplayId)
-                    && mAcknowledgedPointerDisplayId == resolvedDisplayId) {
-                // The requested pointer display is already set.
-                return true;
-            }
-            if (isRemovingOverride && mAcknowledgedPointerDisplayId == Display.INVALID_DISPLAY) {
-                // The pointer display override is being removed, but the current pointer display
-                // is already invalid. This can happen when the PointerController is destroyed as a
-                // result of the removal of all input devices that can control the pointer.
-                return true;
-            }
-            try {
-                // The pointer display changed, so wait until the change has propagated.
-                mAdditionalDisplayInputPropertiesLock.wait(5_000 /*mills*/);
-            } catch (InterruptedException ignored) {
-            }
-            // This request succeeds in two cases:
-            // - This request was to remove the override, in which case the new pointer display
-            //   could be anything that WM has set.
-            // - We are setting a new override, in which case the request only succeeds if the
-            //   reported new displayId is the one we requested. This check ensures that if two
-            //   competing overrides are requested in succession, the caller can be notified if one
-            //   of them fails.
-            return  isRemovingOverride || mAcknowledgedPointerDisplayId == overrideDisplayId;
-        }
-    }
-
-    private int getVirtualMousePointerDisplayId() {
-        if (com.android.input.flags.Flags.enablePointerChoreographer()) {
-            throw new IllegalStateException(
-                    "This must not be used when PointerChoreographer is enabled");
-        }
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            return mOverriddenPointerDisplayId;
-        }
-    }
-
     private void setDisplayEligibilityForPointerCapture(int displayId, boolean isEligible) {
         mNative.setDisplayEligibilityForPointerCapture(displayId, isEligible);
     }
@@ -1714,45 +1606,10 @@
 
     // Binder call
     @Override
-    public void setPointerIconType(int iconType) {
-        if (iconType == PointerIcon.TYPE_CUSTOM) {
-            throw new IllegalArgumentException("Use setCustomPointerIcon to set custom pointers");
-        }
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mPointerIcon = null;
-            mPointerIconType = iconType;
-
-            if (!mCurrentDisplayProperties.pointerIconVisible) return;
-
-            mNative.setPointerIconType(mPointerIconType);
-        }
-    }
-
-    // Binder call
-    @Override
-    public void setCustomPointerIcon(PointerIcon icon) {
-        Objects.requireNonNull(icon);
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mPointerIconType = PointerIcon.TYPE_CUSTOM;
-            mPointerIcon = icon;
-
-            if (!mCurrentDisplayProperties.pointerIconVisible) return;
-
-            mNative.setCustomPointerIcon(mPointerIcon);
-        }
-    }
-
-    // Binder call
-    @Override
     public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId,
             IBinder inputToken) {
         Objects.requireNonNull(icon);
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            mPointerIconType = icon.getType();
-            mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null;
-
-            return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
-        }
+        return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
     }
 
     /**
@@ -1799,7 +1656,8 @@
     }
 
     @Override // Binder call
-    public void addUniqueIdAssociation(@NonNull String inputPort, @NonNull String displayUniqueId) {
+    public void addUniqueIdAssociationByPort(@NonNull String inputPort,
+            @NonNull String displayUniqueId) {
         if (!checkCallingPermission(
                 android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY,
                 "addUniqueIdAssociation()")) {
@@ -1810,13 +1668,13 @@
         Objects.requireNonNull(inputPort);
         Objects.requireNonNull(displayUniqueId);
         synchronized (mAssociationsLock) {
-            mUniqueIdAssociations.put(inputPort, displayUniqueId);
+            mUniqueIdAssociationsByPort.put(inputPort, displayUniqueId);
         }
         mNative.changeUniqueIdAssociation();
     }
 
     @Override // Binder call
-    public void removeUniqueIdAssociation(@NonNull String inputPort) {
+    public void removeUniqueIdAssociationByPort(@NonNull String inputPort) {
         if (!checkCallingPermission(
                 android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY,
                 "removeUniqueIdAssociation()")) {
@@ -1825,7 +1683,7 @@
 
         Objects.requireNonNull(inputPort);
         synchronized (mAssociationsLock) {
-            mUniqueIdAssociations.remove(inputPort);
+            mUniqueIdAssociationsByPort.remove(inputPort);
         }
         mNative.changeUniqueIdAssociation();
     }
@@ -2244,9 +2102,9 @@
                     pw.println("  display: " + v);
                 });
             }
-            if (!mUniqueIdAssociations.isEmpty()) {
+            if (!mUniqueIdAssociationsByPort.isEmpty()) {
                 pw.println("Unique Id Associations:");
-                mUniqueIdAssociations.forEach((k, v) -> {
+                mUniqueIdAssociationsByPort.forEach((k, v) -> {
                     pw.print("  port: " + k);
                     pw.println("  uniqueId: " + v);
                 });
@@ -2281,28 +2139,24 @@
 
     private void dumpDisplayInputPropertiesValues(IndentingPrintWriter pw) {
         synchronized (mAdditionalDisplayInputPropertiesLock) {
-            if (mAdditionalDisplayInputProperties.size() != 0) {
-                pw.println("mAdditionalDisplayInputProperties:");
-                pw.increaseIndent();
+            pw.println("mAdditionalDisplayInputProperties:");
+            pw.increaseIndent();
+            try {
+                if (mAdditionalDisplayInputProperties.size() == 0) {
+                    pw.println("<none>");
+                    return;
+                }
                 for (int i = 0; i < mAdditionalDisplayInputProperties.size(); i++) {
-                    pw.println("displayId: "
-                            + mAdditionalDisplayInputProperties.keyAt(i));
+                    pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i));
                     final AdditionalDisplayInputProperties properties =
                             mAdditionalDisplayInputProperties.valueAt(i);
                     pw.println("mousePointerAccelerationEnabled: "
                             + properties.mousePointerAccelerationEnabled);
                     pw.println("pointerIconVisible: " + properties.pointerIconVisible);
                 }
+            } finally {
                 pw.decreaseIndent();
             }
-            if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
-                pw.println("mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
-            }
-
-            pw.println("mAcknowledgedPointerDisplayId=" + mAcknowledgedPointerDisplayId);
-            pw.println("mRequestedPointerDisplayId=" + mRequestedPointerDisplayId);
-            pw.println("mPointerIconType=" + PointerIcon.typeToString(mPointerIconType));
-            pw.println("mPointerIcon=" + mPointerIcon);
         }
     }
     private boolean checkCallingPermission(String permission, String func) {
@@ -2677,10 +2531,10 @@
 
     // Native callback
     @SuppressWarnings("unused")
-    private String[] getInputUniqueIdAssociations() {
+    private String[] getInputUniqueIdAssociationsByPort() {
         final Map<String, String> associations;
         synchronized (mAssociationsLock) {
-            associations = new HashMap<>(mUniqueIdAssociations);
+            associations = new HashMap<>(mUniqueIdAssociationsByPort);
         }
 
         return flatten(associations);
@@ -2832,9 +2686,7 @@
     @SuppressWarnings("unused")
     @VisibleForTesting
     void onPointerDisplayIdChanged(int pointerDisplayId, float xPosition, float yPosition) {
-        mHandler.obtainMessage(MSG_POINTER_DISPLAY_ID_CHANGED,
-                new PointerDisplayIdChangedArgs(pointerDisplayId, xPosition,
-                        yPosition)).sendToTarget();
+        // TODO(b/311416205): Remove.
     }
 
     @Override
@@ -2989,14 +2841,6 @@
          */
         @Nullable
         SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
-
-        /**
-         * Notify WindowManagerService when the display of the mouse pointer changes.
-         * @param displayId The display on which the mouse pointer is shown.
-         * @param x The x coordinate of the mouse pointer.
-         * @param y The y coordinate of the mouse pointer.
-         */
-        void notifyPointerDisplayIdChanged(int displayId, float x, float y);
     }
 
     /**
@@ -3040,9 +2884,6 @@
                     boolean inTabletMode = (boolean) args.arg1;
                     deliverTabletModeChanged(whenNanos, inTabletMode);
                     break;
-                case MSG_POINTER_DISPLAY_ID_CHANGED:
-                    handlePointerDisplayIdChanged((PointerDisplayIdChangedArgs) msg.obj);
-                    break;
             }
         }
     }
@@ -3267,17 +3108,6 @@
         }
 
         @Override
-        public boolean setVirtualMousePointerDisplayId(int pointerDisplayId) {
-            return InputManagerService.this
-                    .setVirtualMousePointerDisplayIdBlocking(pointerDisplayId);
-        }
-
-        @Override
-        public int getVirtualMousePointerDisplayId() {
-            return InputManagerService.this.getVirtualMousePointerDisplayId();
-        }
-
-        @Override
         public PointF getCursorPosition(int displayId) {
             final float[] p = mNative.getMouseCursorPosition(displayId);
             if (p == null || p.length != 2) {
@@ -3413,44 +3243,6 @@
         }
     }
 
-    private void applyAdditionalDisplayInputProperties() {
-        synchronized (mAdditionalDisplayInputPropertiesLock) {
-            AdditionalDisplayInputProperties properties =
-                    mAdditionalDisplayInputProperties.get(mRequestedPointerDisplayId);
-            if (properties == null) properties = DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES;
-            applyAdditionalDisplayInputPropertiesLocked(properties);
-        }
-    }
-
-    @GuardedBy("mAdditionalDisplayInputPropertiesLock")
-    private void applyAdditionalDisplayInputPropertiesLocked(
-            AdditionalDisplayInputProperties properties) {
-        // Handle changes to each of the individual properties.
-        // TODO(b/293587049): This approach for updating pointer display properties is only for when
-        //  PointerChoreographer is disabled. Remove this logic when PointerChoreographer is
-        //  permanently enabled.
-
-        if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
-            mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
-            if (properties.pointerIconVisible) {
-                if (mPointerIconType == PointerIcon.TYPE_CUSTOM) {
-                    Objects.requireNonNull(mPointerIcon);
-                    mNative.setCustomPointerIcon(mPointerIcon);
-                } else {
-                    mNative.setPointerIconType(mPointerIconType);
-                }
-            } else {
-                mNative.setPointerIconType(PointerIcon.TYPE_NULL);
-            }
-        }
-
-        if (properties.mousePointerAccelerationEnabled
-                != mCurrentDisplayProperties.mousePointerAccelerationEnabled) {
-            mCurrentDisplayProperties.mousePointerAccelerationEnabled =
-                    properties.mousePointerAccelerationEnabled;
-        }
-    }
-
     private void updateAdditionalDisplayInputProperties(int displayId,
             Consumer<AdditionalDisplayInputProperties> updater) {
         synchronized (mAdditionalDisplayInputPropertiesLock) {
@@ -3473,13 +3265,6 @@
             if (properties.allDefaults()) {
                 mAdditionalDisplayInputProperties.remove(displayId);
             }
-            if (displayId != mRequestedPointerDisplayId) {
-                Log.i(TAG, "Not applying additional properties for display " + displayId
-                        + " because the pointer is currently targeting display "
-                        + mRequestedPointerDisplayId + ".");
-                return;
-            }
-            applyAdditionalDisplayInputPropertiesLocked(properties);
         }
     }
 
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 32d5044..f742360 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -189,12 +189,8 @@
 
     void disableInputDevice(int deviceId);
 
-    void setPointerIconType(int iconId);
-
     void reloadPointerIcons();
 
-    void setCustomPointerIcon(@NonNull PointerIcon icon);
-
     boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId,
             @NonNull IBinder inputToken);
 
@@ -467,15 +463,9 @@
         public native void disableInputDevice(int deviceId);
 
         @Override
-        public native void setPointerIconType(int iconId);
-
-        @Override
         public native void reloadPointerIcons();
 
         @Override
-        public native void setCustomPointerIcon(PointerIcon icon);
-
-        @Override
         public native boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId,
                 int pointerId, IBinder inputToken);
 
diff --git a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
index 035a748..00bc751 100644
--- a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
+++ b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
@@ -40,6 +40,13 @@
 
     @NonNull private final InputMethodManagerService mService;
 
+    /**
+     * The host input token of the input method that is currently associated with this controller.
+     */
+    @GuardedBy("ImfLock.class")
+    @Nullable
+    private IBinder mCurHostInputToken;
+
     private static final class CreateInlineSuggestionsRequest {
         @NonNull final InlineSuggestionsRequestInfo mRequestInfo;
         @NonNull final IInlineSuggestionsRequestCallback mCallback;
@@ -78,6 +85,17 @@
     }
 
     @GuardedBy("ImfLock.class")
+    void onResetSystemUi() {
+        mCurHostInputToken = null;
+    }
+
+    @Nullable
+    @GuardedBy("ImfLock.class")
+    IBinder getCurHostInputToken() {
+        return mCurHostInputToken;
+    }
+
+    @GuardedBy("ImfLock.class")
     void onCreateInlineSuggestionsRequest(@UserIdInt int userId,
             InlineSuggestionsRequestInfo requestInfo, IInlineSuggestionsRequestCallback callback,
             boolean touchExplorationEnabled) {
@@ -124,8 +142,7 @@
                             mPendingInlineSuggestionsRequest.mCallback,
                             mPendingInlineSuggestionsRequest.mPackageName,
                             mService.getCurTokenDisplayIdLocked(),
-                            mService.getCurTokenLocked(),
-                            mService);
+                            mService.getCurTokenLocked());
             curMethod.onCreateInlineSuggestionsRequest(
                     mPendingInlineSuggestionsRequest.mRequestInfo, callback);
         } else {
@@ -161,22 +178,20 @@
      * The decorator which validates the host package name in the
      * {@link InlineSuggestionsRequest} argument to make sure it matches the IME package name.
      */
-    private static final class InlineSuggestionsRequestCallbackDecorator
+    private final class InlineSuggestionsRequestCallbackDecorator
             extends IInlineSuggestionsRequestCallback.Stub {
         @NonNull private final IInlineSuggestionsRequestCallback mCallback;
         @NonNull private final String mImePackageName;
         private final int mImeDisplayId;
         @NonNull private final IBinder mImeToken;
-        @NonNull private final InputMethodManagerService mImms;
 
         InlineSuggestionsRequestCallbackDecorator(
                 @NonNull IInlineSuggestionsRequestCallback callback, @NonNull String imePackageName,
-                int displayId, @NonNull IBinder imeToken, @NonNull InputMethodManagerService imms) {
+                int displayId, @NonNull IBinder imeToken) {
             mCallback = callback;
             mImePackageName = imePackageName;
             mImeDisplayId = displayId;
             mImeToken = imeToken;
-            mImms = imms;
         }
 
         @Override
@@ -195,7 +210,12 @@
                                 + "].");
             }
             request.setHostDisplayId(mImeDisplayId);
-            mImms.setCurHostInputToken(mImeToken, request.getHostInputToken());
+            synchronized (ImfLock.class) {
+                final IBinder curImeToken = mService.getCurTokenLocked();
+                if (mImeToken == curImeToken) {
+                    mCurHostInputToken = request.getHostInputToken();
+                }
+            }
             mCallback.onInlineSuggestionsRequest(request, callback);
         }
 
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 7956e03..79f1a9c 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -330,14 +330,10 @@
         mHandwritingSurface.startIntercepting(imePid, imeUid);
 
         // Unset the pointer icon for the stylus in case the app had set it.
-        if (com.android.input.flags.Flags.enablePointerChoreographer()) {
-            Objects.requireNonNull(mContext.getSystemService(InputManager.class)).setPointerIcon(
-                    PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED),
-                    downEvent.getDisplayId(), downEvent.getDeviceId(), downEvent.getPointerId(0),
-                    mHandwritingSurface.getInputChannel().getToken());
-        } else {
-            InputManagerGlobal.getInstance().setPointerIconType(PointerIcon.TYPE_NOT_SPECIFIED);
-        }
+        Objects.requireNonNull(mContext.getSystemService(InputManager.class)).setPointerIcon(
+                PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED),
+                downEvent.getDisplayId(), downEvent.getDeviceId(), downEvent.getPointerId(0),
+                mHandwritingSurface.getInputChannel().getToken());
 
         return new HandwritingSession(mCurrentRequestId, mHandwritingSurface.getInputChannel(),
                 mHandwritingBuffer);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 1d048cb..e8543f2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -150,10 +150,11 @@
      *
      * @param sourceInputToken the source token.
      * @param displayId        the display hosting the IME window
+     * @param userId           the user ID this request is about
      * @return {@code true} if the transfer is successful
      */
     public abstract boolean transferTouchFocusToImeWindow(@NonNull IBinder sourceInputToken,
-            int displayId);
+            int displayId, @UserIdInt int userId);
 
     /**
      * Reports that IME control has transferred to the given window token, or if null that
@@ -287,7 +288,7 @@
 
                 @Override
                 public boolean transferTouchFocusToImeWindow(@NonNull IBinder sourceInputToken,
-                        int displayId) {
+                        int displayId, @UserIdInt int userId) {
                     return false;
                 }
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 1b9d6c5..848f74e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -495,16 +495,6 @@
         return userData.mBindingController.getSequenceNumber();
     }
 
-    /**
-     * Increase the current binding sequence number by one.
-     * Reset to 1 on overflow.
-     */
-    @GuardedBy("ImfLock.class")
-    private void advanceSequenceNumberLocked() {
-        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-        userData.mBindingController.advanceSequenceNumber();
-    }
-
     @GuardedBy("ImfLock.class")
     @Nullable
     InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) {
@@ -583,16 +573,6 @@
     private final WeakHashMap<IBinder, Boolean> mFocusedWindowPerceptible = new WeakHashMap<>();
 
     /**
-     * Set to true if our ServiceConnection is currently actively bound to
-     * a service (whether or not we have gotten its IBinder back yet).
-     */
-    @GuardedBy("ImfLock.class")
-    private boolean hasConnectionLocked() {
-        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-        return userData.mBindingController.hasMainConnection();
-    }
-
-    /**
      * The token tracking the current IME show request that is waiting for a connection to an IME,
      * otherwise {@code null}.
      */
@@ -645,14 +625,6 @@
     private int mCurTokenDisplayId = INVALID_DISPLAY;
 
     /**
-     * The host input token of the current active input method.
-     */
-    @GuardedBy("ImfLock.class")
-    @Nullable
-    @MultiUserUnawareField
-    private IBinder mCurHostInputToken;
-
-    /**
      * The display ID of the input method indicates the fallback display which returned by
      * {@link #computeImeDisplayIdForTarget}.
      */
@@ -679,16 +651,6 @@
     }
 
     /**
-     * Time that we last initiated a bind to the input method, to determine
-     * if we should try to disconnect and reconnect to it.
-     */
-    @GuardedBy("ImfLock.class")
-    private long getLastBindTimeLocked() {
-        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-        return userData.mBindingController.getLastBindTime();
-    }
-
-    /**
      * Have we called mCurMethod.bindInput()?
      */
     @MultiUserUnawareField
@@ -1840,21 +1802,6 @@
     }
 
     /**
-     * Sets current host input token.
-     *
-     * @param callerImeToken the token has been made for the current active input method
-     * @param hostInputToken the host input token of the current active input method
-     */
-    void setCurHostInputToken(@NonNull IBinder callerImeToken, @Nullable IBinder hostInputToken) {
-        synchronized (ImfLock.class) {
-            if (!calledWithValidTokenLocked(callerImeToken)) {
-                return;
-            }
-            mCurHostInputToken = hostInputToken;
-        }
-    }
-
-    /**
      * Gets enabled subtypes of the specified {@link InputMethodInfo}.
      *
      * @param imiId if null, returns enabled subtypes for the current {@link InputMethodInfo}.
@@ -2170,7 +2117,8 @@
             @NonNull EditorInfo editorInfo, @StartInputFlags int startInputFlags,
             @StartInputReason int startInputReason,
             int unverifiedTargetSdkVersion,
-            @NonNull ImeOnBackInvokedDispatcher imeDispatcher) {
+            @NonNull ImeOnBackInvokedDispatcher imeDispatcher,
+            @NonNull UserDataRepository.UserData userData) {
 
         // Compute the final shown display ID with validated cs.selfReportedDisplayId for this
         // session & other conditions.
@@ -2211,7 +2159,8 @@
         final boolean connectionWasActive = mCurInputConnection != null;
 
         // Bump up the sequence for this client and attach it.
-        advanceSequenceNumberLocked();
+        userData.mBindingController.advanceSequenceNumber();
+
         mCurClient = cs;
         mCurInputConnection = inputConnection;
         mCurRemoteAccessibilityInputConnection = remoteAccessibilityInputConnection;
@@ -2233,8 +2182,7 @@
         if (connectionIsActive != connectionWasActive) {
             mInputManagerInternal.notifyInputMethodConnectionActive(connectionIsActive);
         }
-        final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-
+        final var bindingController = userData.mBindingController;
 
         // If configured, we want to avoid starting up the IME if it is not supposed to be showing
         if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags,
@@ -2243,14 +2191,16 @@
                 Slog.d(TAG, "Avoiding IME startup and unbinding current input method.");
             }
             invalidateAutofillSessionLocked();
-            userData.mBindingController.unbindCurrentMethod();
+            bindingController.unbindCurrentMethod();
             return InputBindResult.NO_EDITOR;
         }
 
         // Check if the input method is changing.
         // We expect the caller has already verified that the client is allowed to access this
         // display ID.
-        if (isSelectedMethodBoundLocked()) {
+        final String curId = bindingController.getCurId();
+        if (curId != null && curId.equals(bindingController.getSelectedMethodId())
+                && mDisplayIdToShowIme == mCurTokenDisplayId) {
             if (cs.mCurSession != null) {
                 // Fast case: if we are already connected to the input method,
                 // then just return it.
@@ -2269,14 +2219,14 @@
                         (startInputFlags & StartInputFlags.INITIAL_CONNECTION) != 0);
             }
 
-            InputBindResult bindResult = tryReuseConnectionLocked(cs);
+            InputBindResult bindResult = tryReuseConnectionLocked(userData, cs);
             if (bindResult != null) {
                 return bindResult;
             }
         }
 
-        userData.mBindingController.unbindCurrentMethod();
-        return userData.mBindingController.bindCurrentMethod();
+        bindingController.unbindCurrentMethod();
+        return bindingController.bindCurrentMethod();
     }
 
     /**
@@ -2369,13 +2319,6 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean isSelectedMethodBoundLocked() {
-        String curId = getCurIdLocked();
-        return curId != null && curId.equals(getSelectedMethodIdLocked())
-                && mDisplayIdToShowIme == mCurTokenDisplayId;
-    }
-
-    @GuardedBy("ImfLock.class")
     private void prepareClientSwitchLocked(ClientState cs) {
         // If the client is changing, we need to switch over to the new
         // one.
@@ -2388,8 +2331,9 @@
 
     @GuardedBy("ImfLock.class")
     @Nullable
-    private InputBindResult tryReuseConnectionLocked(@NonNull ClientState cs) {
-        if (hasConnectionLocked()) {
+    private InputBindResult tryReuseConnectionLocked(@NonNull UserDataRepository.UserData userData,
+            @NonNull ClientState cs) {
+        if (userData.mBindingController.hasMainConnection()) {
             if (getCurMethodLocked() != null) {
                 // Return to client, and we will get back with it when
                 // we have had a session made for it.
@@ -2399,7 +2343,8 @@
                         InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
                         null, null, null, getCurIdLocked(), getSequenceNumberLocked(), false);
             } else {
-                long bindingDuration = SystemClock.uptimeMillis() - getLastBindTimeLocked();
+                final long lastBindTime = userData.mBindingController.getLastBindTime();
+                long bindingDuration = SystemClock.uptimeMillis() - lastBindTime;
                 if (bindingDuration < TIME_TO_RECONNECT) {
                     // In this case we have connected to the service, but
                     // don't yet have its interface.  If it hasn't been too
@@ -2532,7 +2477,7 @@
         mBackDisposition = InputMethodService.BACK_DISPOSITION_DEFAULT;
         updateSystemUiLocked(mImeWindowVis, mBackDisposition);
         mCurTokenDisplayId = INVALID_DISPLAY;
-        mCurHostInputToken = null;
+        mAutofillController.onResetSystemUi();
     }
 
     @GuardedBy("ImfLock.class")
@@ -3779,6 +3724,7 @@
                 startInputByWinGainedFocus, toolType);
         mVisibilityStateComputer.setWindowState(windowToken, windowState);
 
+        final var userData = mUserDataRepository.getOrCreate(userId);
         if (sameWindowFocused && isTextEditor) {
             if (DEBUG) {
                 Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
@@ -3789,7 +3735,7 @@
             if (editorInfo != null) {
                 return startInputUncheckedLocked(cs, inputContext,
                         remoteAccessibilityInputConnection, editorInfo, startInputFlags,
-                        startInputReason, unverifiedTargetSdkVersion, imeDispatcher);
+                        startInputReason, unverifiedTargetSdkVersion, imeDispatcher, userData);
             }
             return new InputBindResult(
                     InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
@@ -3821,7 +3767,7 @@
                         res = startInputUncheckedLocked(cs, inputContext,
                                 remoteAccessibilityInputConnection, editorInfo, startInputFlags,
                                 startInputReason, unverifiedTargetSdkVersion,
-                                imeDispatcher);
+                                imeDispatcher, userData);
                         didStart = true;
                     }
                     break;
@@ -3836,7 +3782,6 @@
                 // Note that we can trust client's display ID as long as it matches
                 // to the display ID obtained from the window.
                 if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) {
-                    final var userData = mUserDataRepository.getOrCreate(userId);
                     userData.mBindingController.unbindCurrentMethod();
                 }
             }
@@ -3846,7 +3791,7 @@
                 res = startInputUncheckedLocked(cs, inputContext,
                         remoteAccessibilityInputConnection, editorInfo, startInputFlags,
                         startInputReason, unverifiedTargetSdkVersion,
-                        imeDispatcher);
+                        imeDispatcher, userData);
             } else {
                 res = InputBindResult.NULL_EDITOR_INFO;
             }
@@ -4481,6 +4426,7 @@
 
     private void dumpDebug(ProtoOutputStream proto, long fieldId) {
         synchronized (ImfLock.class) {
+            final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
             final long token = proto.start(fieldId);
             proto.write(CUR_METHOD_ID, getSelectedMethodIdLocked());
             proto.write(CUR_SEQ, getSequenceNumberLocked());
@@ -4499,7 +4445,7 @@
             proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked()));
             proto.write(CUR_TOKEN_DISPLAY_ID, mCurTokenDisplayId);
             proto.write(SYSTEM_READY, mSystemReady);
-            proto.write(HAVE_CONNECTION, hasConnectionLocked());
+            proto.write(HAVE_CONNECTION, userData.mBindingController.hasMainConnection());
             proto.write(BOUND_TO_METHOD, mBoundToMethod);
             proto.write(IS_INTERACTIVE, mIsInteractive);
             proto.write(BACK_DISPOSITION, mBackDisposition);
@@ -5625,14 +5571,17 @@
 
         @Override
         public boolean transferTouchFocusToImeWindow(@NonNull IBinder sourceInputToken,
-                int displayId) {
+                int displayId, @UserIdInt int userId) {
             //TODO(b/150843766): Check if Input Token is valid.
             final IBinder curHostInputToken;
             synchronized (ImfLock.class) {
-                if (displayId != mCurTokenDisplayId || mCurHostInputToken == null) {
+                if (displayId != mCurTokenDisplayId) {
                     return false;
                 }
-                curHostInputToken = mCurHostInputToken;
+                curHostInputToken = mAutofillController.getCurHostInputToken();
+                if (curHostInputToken == null) {
+                    return false;
+                }
             }
             return mInputManagerInternal.transferTouchGesture(sourceInputToken, curHostInputToken);
         }
@@ -5947,14 +5896,27 @@
             client = mCurClient;
             p.println("  mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked());
             p.println("  mFocusedWindowPerceptible=" + mFocusedWindowPerceptible);
-            mImeBindingState.dump("  ", p);
+            mImeBindingState.dump(/* prefix= */ "  ", p);
             final var userData = mUserDataRepository.getOrCreate(mCurrentUserId);
-            p.println("  mCurId=" + getCurIdLocked() + " mHaveConnection=" + hasConnectionLocked()
+            p.println("  mCurId=" + getCurIdLocked()
+                    + " mHaveConnection=" + userData.mBindingController.hasMainConnection()
                     + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound="
                     + userData.mBindingController.isVisibleBound());
+
+            p.println("  mUserDataRepository=");
+            // TODO(b/324907325): Remove the suppress warnings once b/324907325 is fixed.
+            @SuppressWarnings("GuardedBy") Consumer<UserDataRepository.UserData> userDataDump =
+                    u -> {
+                        p.println("    mUserId=" + u.mUserId);
+                        p.println("      hasMainConnection="
+                                + u.mBindingController.hasMainConnection());
+                        p.println("      isVisibleBound=" + u.mBindingController.isVisibleBound());
+                    };
+            mUserDataRepository.forAllUserData(userDataDump);
+
             p.println("  mCurToken=" + getCurTokenLocked());
             p.println("  mCurTokenDisplayId=" + mCurTokenDisplayId);
-            p.println("  mCurHostInputToken=" + mCurHostInputToken);
+            p.println("  mCurHostInputToken=" + mAutofillController.getCurHostInputToken());
             p.println("  mCurIntent=" + getCurIntentLocked());
             method = getCurMethodLocked();
             p.println("  mCurMethod=" + getCurMethodLocked());
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index 0049213..d932bd4 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -32,6 +32,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.os.Bundle;
 import android.os.Environment;
 import android.os.HandlerThread;
 import android.os.LocaleList;
@@ -101,6 +102,11 @@
     // the application setting the app-locale itself.
     private final SharedPreferences mDelegateAppLocalePackages;
     private final BroadcastReceiver mUserMonitor;
+    // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon receiving
+    // the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform the data
+    // restoration during the second PACKAGE_ADDED broadcast, which is sent subsequently when the
+    // app is installed.
+    private final Set<String> mPkgsToRestore;
 
     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
             PackageManager packageManager, HandlerThread broadcastHandlerThread) {
@@ -119,6 +125,7 @@
         mStagedData = stagedData;
         mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
                 : createPersistedInfo();
+        mPkgsToRestore = new ArraySet<>();
 
         mUserMonitor = new UserMonitor();
         IntentFilter filter = new IntentFilter();
@@ -251,6 +258,9 @@
                 LocalesInfo localesInfo = pkgStates.get(pkgName);
                 // Check if the application is already installed for the concerned user.
                 if (isPackageInstalledForUser(pkgName, userId)) {
+                    if (mPkgsToRestore != null) {
+                        mPkgsToRestore.remove(pkgName);
+                    }
                     // Don't apply the restore if the locales have already been set for the app.
                     checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
                 } else {
@@ -279,23 +289,18 @@
 
     /**
      * <p><b>Note:</b> This is invoked by service's common monitor
-     * {@link LocaleManagerServicePackageMonitor#onPackageAdded} when a new package is
+     * {@link LocaleManagerServicePackageMonitor#onPackageAddedWithExtras} when a new package is
      * added on device.
      */
-    void onPackageAdded(String packageName, int uid) {
-        try {
-            synchronized (mStagedDataLock) {
-                cleanStagedDataForOldEntriesLocked();
-
-                int userId = UserHandle.getUserId(uid);
-                if (mStagedData.contains(userId)) {
-                    // Perform lazy restore only if the staged data exists.
-                    doLazyRestoreLocked(packageName, userId);
-                }
+    void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
+        boolean archived = false;
+        if (extras != null) {
+            archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false);
+            if (archived && mPkgsToRestore != null) {
+                mPkgsToRestore.add(packageName);
             }
-        } catch (Exception e) {
-            Slog.e(TAG, "Exception in onPackageAdded.", e);
         }
+        checkStageDataAndApplyRestore(packageName, uid);
     }
 
     /**
@@ -305,6 +310,10 @@
      */
     void onPackageUpdateFinished(String packageName, int uid) {
         int userId = UserHandle.getUserId(uid);
+        if (mPkgsToRestore != null && mPkgsToRestore.contains(packageName)) {
+            mPkgsToRestore.remove(packageName);
+            checkStageDataAndApplyRestore(packageName, uid);
+        }
         cleanApplicationLocalesIfNeeded(packageName, userId);
     }
 
@@ -338,6 +347,25 @@
         }
     }
 
+    private void checkStageDataAndApplyRestore(String packageName, int uid) {
+        try {
+            synchronized (mStagedDataLock) {
+                cleanStagedDataForOldEntriesLocked();
+
+                int userId = UserHandle.getUserId(uid);
+                if (mStagedData.contains(userId)) {
+                    if (mPkgsToRestore != null) {
+                        mPkgsToRestore.remove(packageName);
+                    }
+                    // Perform lazy restore only if the staged data exists.
+                    doLazyRestoreLocked(packageName, userId);
+                }
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Exception in onPackageAdded.", e);
+        }
+    }
+
     private boolean isPackageInstalledForUser(String packageName, int userId) {
         PackageInfo pkgInfo = null;
         try {
diff --git a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
index ecd3614..e0a050f 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
@@ -17,6 +17,7 @@
 package com.android.server.locales;
 
 import android.annotation.NonNull;
+import android.os.Bundle;
 import android.os.UserHandle;
 
 import com.android.internal.content.PackageMonitor;
@@ -48,8 +49,8 @@
     }
 
     @Override
-    public void onPackageAdded(String packageName, int uid) {
-        mBackupHelper.onPackageAdded(packageName, uid);
+    public void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
+        mBackupHelper.onPackageAddedWithExtras(packageName, uid, extras);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 5731161..17f8abe 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -78,10 +78,14 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.PriorityQueue;
+import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -152,6 +156,16 @@
     private final ScheduledThreadPoolExecutor mDailyMetricTimer =
             new ScheduledThreadPoolExecutor(1);
 
+    // A queue of reliable message records for duplicate detection
+    private final PriorityQueue<ReliableMessageRecord> mReliableMessageRecordQueue =
+            new PriorityQueue<ReliableMessageRecord>(
+                    (ReliableMessageRecord left, ReliableMessageRecord right) -> {
+                        return Long.compare(left.getTimestamp(), right.getTimestamp());
+                    });
+
+    // The test mode manager that manages behaviors during test mode.
+    private final TestModeManager mTestModeManager = new TestModeManager();
+
     // The period of the recurring time
     private static final int PERIOD_METRIC_QUERY_DAYS = 1;
 
@@ -164,6 +178,9 @@
     private boolean mIsBtScanningEnabled = false;
     private boolean mIsBtMainEnabled = false;
 
+    // True if test mode is enabled for the Context Hub
+    private AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
+
     // A hashmap used to record if a contexthub is waiting for daily query
     private Set<Integer> mMetricQueryPendingContextHubIds =
             Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
@@ -210,8 +227,17 @@
         @Override
         public void handleNanoappMessage(short hostEndpointId, NanoAppMessage message,
                 List<String> nanoappPermissions, List<String> messagePermissions) {
-            handleClientMessageCallback(mContextHubId, hostEndpointId, message, nanoappPermissions,
-                    messagePermissions);
+            if (Flags.reliableMessageImplementation()
+                    && Flags.reliableMessageTestModeBehavior()
+                    && mIsTestModeEnabled.get()
+                    && mTestModeManager.handleNanoappMessage(mContextHubId, hostEndpointId,
+                            message, nanoappPermissions, messagePermissions)) {
+                // The TestModeManager handled the nanoapp message, so return here.
+                return;
+            }
+
+            handleClientMessageCallback(mContextHubId, hostEndpointId, message,
+                    nanoappPermissions, messagePermissions);
         }
 
         @Override
@@ -228,6 +254,111 @@
         }
     }
 
+    /**
+     * Records a reliable message from a nanoapp for duplicate detection.
+     */
+    private static class ReliableMessageRecord {
+        public static final int TIMEOUT_NS = 1000000000;
+
+        public int mContextHubId;
+        public long mTimestamp;
+        public int mMessageSequenceNumber;
+        byte mErrorCode;
+
+        ReliableMessageRecord(int contextHubId, long timestamp,
+                int messageSequenceNumber, byte errorCode) {
+            mContextHubId = contextHubId;
+            mTimestamp = timestamp;
+            mMessageSequenceNumber = messageSequenceNumber;
+            mErrorCode = errorCode;
+        }
+
+        public int getContextHubId() {
+            return mContextHubId;
+        }
+
+        public long getTimestamp() {
+            return mTimestamp;
+        }
+
+        public int getMessageSequenceNumber() {
+            return mMessageSequenceNumber;
+        }
+
+        public byte getErrorCode() {
+            return mErrorCode;
+        }
+
+        public void setErrorCode(byte errorCode) {
+            mErrorCode = errorCode;
+        }
+
+        public boolean isExpired() {
+            return mTimestamp + TIMEOUT_NS < SystemClock.elapsedRealtimeNanos();
+        }
+    }
+
+    /**
+     * A class to manage behaviors during test mode. This is used for testing.
+     */
+    private class TestModeManager {
+        /**
+         * Probability (in percent) of duplicating a message.
+         */
+        private static final int MESSAGE_DUPLICATION_PROBABILITY_PERCENT = 50;
+
+        /**
+         * The number of total messages to send when the duplicate event happens.
+         */
+        private static final int NUM_MESSAGES_TO_DUPLICATE = 3;
+
+        /**
+         * A probability percent for a certain event.
+         */
+        private static final int MAX_PROBABILITY_PERCENT = 100;
+
+        /**
+         * Random number generator.
+         */
+        private Random mRandom = new Random();
+
+        /**
+         * @see ContextHubServiceCallback.handleNanoappMessage
+         * @return whether the message was handled
+         */
+        public boolean handleNanoappMessage(int contextHubId,
+                short hostEndpointId, NanoAppMessage message,
+                List<String> nanoappPermissions, List<String> messagePermissions) {
+            if (!message.isReliable()) {
+                return false;
+            }
+
+            if (Flags.reliableMessageDuplicateDetectionService()
+                && didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) {
+                Log.i(TAG, "[TEST MODE] Duplicating message ("
+                        + NUM_MESSAGES_TO_DUPLICATE
+                        + " sends) with message sequence number: "
+                        + message.getMessageSequenceNumber());
+                for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) {
+                    handleClientMessageCallback(contextHubId, hostEndpointId,
+                            message, nanoappPermissions, messagePermissions);
+                }
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Returns true if the event with percentPercent did happen.
+         *
+         * @param probabilityPercent the percent probability of the event.
+         * @return true if the event happened, false otherwise.
+         */
+        private boolean didEventHappen(int probabilityPercent) {
+            return mRandom.nextInt(MAX_PROBABILITY_PERCENT) < probabilityPercent;
+        }
+    }
+
     public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
         Log.i(TAG, "Starting Context Hub Service init");
         mContext = context;
@@ -563,6 +694,8 @@
      * Resets the settings. Called when a context hub restarts or the AIDL HAL dies
      */
     private void resetSettings() {
+        mIsTestModeEnabled.set(false);
+
         sendLocationSettingUpdate();
         sendWifiSettingUpdate(/* forceUpdate= */ true);
         sendAirplaneModeSettingUpdate();
@@ -854,14 +987,76 @@
     private void handleClientMessageCallback(int contextHubId, short hostEndpointId,
             NanoAppMessage message, List<String> nanoappPermissions,
             List<String> messagePermissions) {
-        byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId, message,
-                nanoappPermissions, messagePermissions);
-        if (message.isReliable() && errorCode != ErrorCode.OK) {
-            sendMessageDeliveryStatusToContextHub(contextHubId, message.getMessageSequenceNumber(),
-                    errorCode);
+        if (!Flags.reliableMessageImplementation()
+                || !Flags.reliableMessageDuplicateDetectionService()) {
+            byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId,
+                    message, nanoappPermissions, messagePermissions);
+            if (message.isReliable() && errorCode != ErrorCode.OK) {
+                sendMessageDeliveryStatusToContextHub(contextHubId,
+                        message.getMessageSequenceNumber(), errorCode);
+            }
+            return;
+        }
+
+        if (message.isReliable()) {
+            byte errorCode = ErrorCode.OK;
+            synchronized (mReliableMessageRecordQueue) {
+                Optional<ReliableMessageRecord> record = Optional.empty();
+                for (ReliableMessageRecord r: mReliableMessageRecordQueue) {
+                    if (r.getContextHubId() == contextHubId
+                            && r.getMessageSequenceNumber() == message.getMessageSequenceNumber()) {
+                        record = Optional.of(r);
+                        break;
+                    }
+                }
+
+                if (record.isPresent()) {
+                    errorCode = record.get().getErrorCode();
+                    if (errorCode == ErrorCode.TRANSIENT_ERROR) {
+                        Log.w(TAG, "Found duplicate reliable message with message sequence number: "
+                                + record.get().getMessageSequenceNumber() + ": retrying");
+                        errorCode = mClientManager.onMessageFromNanoApp(
+                                contextHubId, hostEndpointId, message,
+                                nanoappPermissions, messagePermissions);
+                        record.get().setErrorCode(errorCode);
+                    } else {
+                        Log.w(TAG, "Found duplicate reliable message with message sequence number: "
+                                + record.get().getMessageSequenceNumber());
+                    }
+                } else {
+                    errorCode = mClientManager.onMessageFromNanoApp(
+                            contextHubId, hostEndpointId, message,
+                            nanoappPermissions, messagePermissions);
+                    mReliableMessageRecordQueue.add(
+                            new ReliableMessageRecord(contextHubId,
+                                    SystemClock.elapsedRealtimeNanos(),
+                                    message.getMessageSequenceNumber(),
+                                    errorCode));
+                }
+            }
+            sendMessageDeliveryStatusToContextHub(contextHubId,
+                    message.getMessageSequenceNumber(), errorCode);
+        } else {
+            mClientManager.onMessageFromNanoApp(
+                    contextHubId, hostEndpointId, message,
+                    nanoappPermissions, messagePermissions);
+        }
+
+        synchronized (mReliableMessageRecordQueue) {
+            while (mReliableMessageRecordQueue.peek() != null
+                   && mReliableMessageRecordQueue.peek().isExpired()) {
+                mReliableMessageRecordQueue.poll();
+            }
         }
     }
 
+    /**
+     * Sends the message delivery status to the Context Hub.
+     *
+     * @param contextHubId the ID of the hub
+     * @param messageSequenceNumber the message sequence number
+     * @param errorCode the error code, one of the enum ErrorCode
+     */
     private void sendMessageDeliveryStatusToContextHub(int contextHubId,
             int messageSequenceNumber, byte errorCode) {
         if (!Flags.reliableMessageImplementation()) {
@@ -1229,6 +1424,9 @@
     public boolean setTestMode(boolean enable) {
         super.setTestMode_enforcePermission();
         boolean status = mContextHubWrapper.setTestMode(enable);
+        if (status) {
+            mIsTestModeEnabled.set(enable);
+        }
 
         // Query nanoapps to update service state after test mode state change.
         for (int contextHubId: mDefaultClientMap.keySet()) {
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 869b89a..73647db 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1305,22 +1305,20 @@
                         route.getId(),
                         requestId));
 
+        UserHandler userHandler = routerRecord.mUserRecord.mHandler;
         if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) {
-            ManagerRecord manager = routerRecord.mUserRecord.mHandler.findManagerWithId(
-                    toRequesterId(managerRequestId));
+            ManagerRecord manager = userHandler.findManagerWithId(toRequesterId(managerRequestId));
             if (manager == null || manager.mLastSessionCreationRequest == null) {
                 Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
                         + "Ignoring unknown request.");
-                routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
-                        routerRecord, requestId);
+                userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
                 return;
             }
             if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(),
                     oldSession.getId())) {
                 Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
                         + "Ignoring unmatched routing session.");
-                routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
-                        routerRecord, requestId);
+                userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
                 return;
             }
             if (!TextUtils.equals(manager.mLastSessionCreationRequest.mRoute.getId(),
@@ -1333,29 +1331,28 @@
                 } else {
                     Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
                             + "Ignoring unmatched route.");
-                    routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
-                            routerRecord, requestId);
+                    userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
                     return;
                 }
             }
             manager.mLastSessionCreationRequest = null;
         } else {
+            String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId();
             if (route.isSystemRoute()
                     && !routerRecord.hasSystemRoutingPermission()
-                    && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+                    && !TextUtils.equals(route.getId(), defaultRouteId)) {
                 Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to"
                         + route);
-                routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
-                        routerRecord, requestId);
+                userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
                 return;
             }
         }
 
         long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
-        routerRecord.mUserRecord.mHandler.sendMessage(
+        userHandler.sendMessage(
                 obtainMessage(
                         UserHandler::requestCreateSessionWithRouter2OnHandler,
-                        routerRecord.mUserRecord.mHandler,
+                        userHandler,
                         uniqueRequestId,
                         managerRequestId,
                         transferInitiatorUserHandle,
@@ -1429,18 +1426,22 @@
                         "transferToRouteWithRouter2 | router: %s(id: %d), route: %s",
                         routerRecord.mPackageName, routerRecord.mRouterId, route.getId()));
 
+        UserHandler userHandler = routerRecord.mUserRecord.mHandler;
+        String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId();
         if (route.isSystemRoute()
                 && !routerRecord.hasSystemRoutingPermission()
-                && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
-            routerRecord.mUserRecord.mHandler.sendMessage(
-                    obtainMessage(UserHandler::notifySessionCreationFailedToRouter,
-                            routerRecord.mUserRecord.mHandler,
-                            routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID)));
+                && !TextUtils.equals(route.getId(), defaultRouteId)) {
+            userHandler.sendMessage(
+                    obtainMessage(
+                            UserHandler::notifySessionCreationFailedToRouter,
+                            userHandler,
+                            routerRecord,
+                            toOriginalRequestId(DUMMY_REQUEST_ID)));
         } else {
-            routerRecord.mUserRecord.mHandler.sendMessage(
+            userHandler.sendMessage(
                     obtainMessage(
                             UserHandler::transferToRouteOnHandler,
-                            routerRecord.mUserRecord.mHandler,
+                            userHandler,
                             DUMMY_REQUEST_ID,
                             transferInitiatorUserHandle,
                             routerRecord.mPackageName,
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index c105b9c..6ce3ab4 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -232,10 +232,16 @@
             String sessionId,
             String routeId,
             @RoutingSessionInfo.TransferReason int transferReason) {
+        String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId();
         if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
-            // The currently selected route is the default route.
-            Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
-            return;
+            if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+                // Transfer to the default route (which is the selected route). We replace the id to
+                // be the selected route id so that the transfer reason gets updated.
+                routeId = selectedDeviceRouteId;
+            } else {
+                Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
+                return;
+            }
         }
 
         if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
@@ -250,11 +256,11 @@
             }
         }
 
-        MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
+        String finalRouteId = routeId; // Make a final copy to use it in the lambda.
         boolean isAvailableDeviceRoute =
                 mDeviceRouteController.getAvailableRoutes().stream()
-                        .anyMatch(it -> it.getId().equals(routeId));
-        boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRoute.getId());
+                        .anyMatch(it -> it.getId().equals(finalRouteId));
+        boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId);
 
         if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
             // The requested route is managed by the device route controller. Note that the selected
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 42ec1c3..61054a9 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -11271,6 +11271,9 @@
 
             // Lifetime extended notifications don't need to alert on state change.
             record.setPostSilently(true);
+            // We also set FLAG_ONLY_ALERT_ONCE to avoid the notification from HUN-ing again.
+            record.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
+
             mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
                     record, isAppForeground,
                     mPostNotificationTrackerFactory.newTracker(null)));
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 143bc5c..b589f49 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -486,6 +486,7 @@
         }
     }
 
+    @GuardedBy("mConfigLock")
     private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd,
             AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) {
         if (!Flags.modesApi()) {
@@ -1112,6 +1113,7 @@
      * <p>The rule's {@link ZenRule#condition} is cleared (meaning that an active rule will be
      * deactivated) unless the update has origin == {@link ZenModeConfig#UPDATE_ORIGIN_USER}.
      */
+    @GuardedBy("mConfigLock")
     private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenRule rule,
                          @ConfigChangeOrigin int origin, boolean isNew) {
         if (Flags.modesApi()) {
@@ -1261,12 +1263,14 @@
      *
      * <p>Returns {@code true} if the policy of the rule was modified.
      */
+    @GuardedBy("mConfigLock")
     private boolean updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
             boolean updateBitmask, boolean isNew) {
         if (newPolicy == null) {
             if (isNew) {
                 // Newly created rule with no provided policy; fill in with the default.
-                zenRule.zenPolicy = mDefaultConfig.toZenPolicy();
+                zenRule.zenPolicy =
+                        Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy();
                 return true;
             }
             // Otherwise, a null policy means no policy changes, so we can stop here.
@@ -1275,8 +1279,9 @@
 
         // If oldPolicy is null, we compare against the default policy when determining which
         // fields in the bitmask should be marked as updated.
-        ZenPolicy oldPolicy =
-                zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy();
+        ZenPolicy oldPolicy = zenRule.zenPolicy != null
+                ? zenRule.zenPolicy
+                : (Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy());
 
         // If this is updating a rule rather than creating a new one, keep any fields from the
         // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy
@@ -2033,7 +2038,8 @@
                     // rule's policy fields should be set upon creation, this is a fallback to
                     // catch any that may have fallen through the cracks.
                     Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
-                    policy.apply(mDefaultConfig.toZenPolicy());
+                    policy.apply(
+                            Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy());
                 }
             } else {
                 // active rule with no specified policy inherits the global config settings
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index c60f0af..209cbb7 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -46,6 +46,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ApexStagedEvent;
+import android.content.pm.Flags;
 import android.content.pm.IPackageManagerNative;
 import android.content.pm.IStagedApexObserver;
 import android.content.pm.PackageManager;
@@ -766,6 +767,10 @@
         final PackageSetting ps = installRequest.getScannedPackageSetting();
         final AndroidPackage pkg = ps.getPkg();
         final boolean onIncremental = isIncrementalPath(ps.getPathString());
+        final boolean performDexOptForRollback = Flags.recoverabilityDetection()
+                ? !(installRequest.isRollback()
+                && installRequest.getInstallSource().mInitiatingPackageName.equals("android"))
+                : true;
 
         return (!instantApp || Global.getInt(context.getContentResolver(),
                 Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
@@ -773,7 +778,8 @@
                 && !pkg.isDebuggable()
                 && (!onIncremental)
                 && dexoptOptions.isCompilationEnabled()
-                && !isApex;
+                && !isApex
+                && performDexOptForRollback;
     }
 
     private static class StagedApexObserver extends IStagedApexObserver.Stub {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 1793794..7a36f6d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -35,6 +35,8 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.role.RoleManager;
+import android.app.usage.StorageStats;
+import android.app.usage.StorageStatsManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.IIntentReceiver;
@@ -136,6 +138,7 @@
 import java.io.PrintWriter;
 import java.net.URISyntaxException;
 import java.security.SecureRandom;
+import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
@@ -275,6 +278,8 @@
                     return runClear();
                 case "get-archived-package-metadata":
                     return runGetArchivedPackageMetadata();
+                case "get-package-storage-stats":
+                    return runGetPackageStorageStats();
                 case "install-archived":
                     return runArchivedInstall();
                 case "enable":
@@ -1861,6 +1866,103 @@
         return 0;
     }
 
+    /**
+     * Returns a string that shows the number of bytes in b, Kb, Mb or Gb.
+     */
+    protected static String getFormattedBytes(long size) {
+        double k = size/1024.0;
+        double m = size/1048576.0;
+        double g = size/1073741824.0;
+
+        DecimalFormat dec = new DecimalFormat("0.00");
+        if (g > 1) {
+            return dec.format(g).concat(" Gb");
+        } else if (m > 1) {
+            return dec.format(m).concat(" Mb");
+        } else if (k > 1) {
+            return dec.format(k).concat(" Kb");
+        }
+        return "";
+    }
+
+    /**
+     * Return the string that displays the data size.
+     */
+    private String getDataSizeDisplay(long size) {
+        String formattedOutput = getFormattedBytes(size);
+        if (!formattedOutput.isEmpty()) {
+           formattedOutput = " (" + formattedOutput + ")";
+        }
+        return Long.toString(size) + " bytes" + formattedOutput;
+    }
+
+    /**
+     * Display storage stats of the specified package.
+     *
+     * Usage: get-package-storage-stats [--usr USER_ID] PACKAGE
+     */
+    private int runGetPackageStorageStats() throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
+        if (!android.content.pm.Flags.getPackageStorageStats()) {
+            pw.println("Error: get_package_storage_stats flag is not enabled");
+            return 1;
+        }
+        if (!android.app.usage.Flags.getAppBytesByDataTypeApi()) {
+            pw.println("Error: get_app_bytes_by_data_type_api flag is not enabled");
+            return 1;
+        }
+        int userId = UserHandle.USER_CURRENT;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "--user":
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+                default:
+                    pw.println("Error: Unknown option: " + opt);
+                    return 1;
+            }
+        }
+
+        final String packageName = getNextArg();
+        if (packageName == null) {
+            pw.println("Error: package name not specified");
+            return 1;
+        }
+        try {
+            StorageStatsManager storageStatsManager =
+                mContext.getSystemService(StorageStatsManager.class);
+            final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL,
+                "runGetPackageStorageStats");
+            StorageStats stats =
+                storageStatsManager.queryStatsForPackage(StorageManager.UUID_DEFAULT,
+                    packageName, UserHandle.of(translatedUserId));
+
+            pw.println("code: " + getDataSizeDisplay(stats.getAppBytes()));
+            pw.println("data: " + getDataSizeDisplay(stats.getDataBytes()));
+            pw.println("cache: " + getDataSizeDisplay(stats.getCacheBytes()));
+            pw.println("apk: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+                StorageStats.APP_DATA_TYPE_FILE_TYPE_APK)));
+            pw.println("lib: " + getDataSizeDisplay(
+                stats.getAppBytesByDataType(StorageStats.APP_DATA_TYPE_LIB)));
+            pw.println("dm: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+                StorageStats.APP_DATA_TYPE_FILE_TYPE_DM)));
+            pw.println("dexopt artifacts: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+                StorageStats.APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT)));
+            pw.println("current profile : " + getDataSizeDisplay(stats.getAppBytesByDataType(
+                StorageStats.APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE)));
+            pw.println("reference profile: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+                StorageStats.APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE)));
+            pw.println("external cache: " + getDataSizeDisplay(stats.getExternalCacheBytes()));
+        } catch (Exception e) {
+            getErrPrintWriter().println("Failed to get storage stats, reason: " + e);
+            pw.println("Failure [failed to get storage stats], reason: " + e);
+            return -1;
+        }
+        return 0;
+    }
+
     private int runInstallExisting() throws RemoteException {
         final PrintWriter pw = getOutPrintWriter();
         int userId = UserHandle.USER_CURRENT;
@@ -4869,6 +4971,8 @@
         pw.println("    Displays the component name of the domain verification agent on device.");
         pw.println("    If the component isn't enabled, an error message will be displayed.");
         pw.println("      --user: return the agent of the given user (SYSTEM_USER if unspecified)");
+        pw.println("  get-package-storage-stats [--user <USER_ID>] <PACKAGE>");
+        pw.println("    Return the storage stats for the given app, if present");
         pw.println("");
         printArtServiceHelp();
         pw.println("");
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 1309e44..41d6288 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -2139,10 +2139,17 @@
                     continue;
                 }
 
+                ComponentName unflattenOriginalComponentName = ComponentName.unflattenFromString(
+                        originalComponentName);
+                if (unflattenOriginalComponentName == null) {
+                    Slog.d(TAG, "Incorrect component name from the attributes");
+                    continue;
+                }
+
                 activityInfos.add(
                         new ArchiveState.ArchiveActivityInfo(
                                 title,
-                                ComponentName.unflattenFromString(originalComponentName),
+                                unflattenOriginalComponentName,
                                 iconPath,
                                 monochromeIconPath));
             }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index b1976cd..f6487ce 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1371,7 +1371,7 @@
             for (int i = 0; i < userSize; i++) {
                 UserInfo ui = mUsers.valueAt(i).info;
                 if ((excludePartial && ui.partial)
-                        || (excludeDying && mRemovingUserIds.get(ui.id))
+                        || (excludeDying && isDyingLU(ui))
                         || (excludePreCreated && ui.preCreated)) {
                     continue;
                 }
@@ -1381,6 +1381,17 @@
         }
     }
 
+    @GuardedBy("mUsersLock")
+    private boolean isDyingLU(UserInfo ui) {
+        if (mRemovingUserIds.get(ui.id)) {
+            return true;
+        }
+        if (ui.isEphemeral() && ui.isInitialized() && ui.id != getCurrentUserId()) {
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public List<UserInfo> getProfiles(@UserIdInt int userId, boolean enabledOnly) {
         boolean returnFullInfo;
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 4e02470..483d308 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -859,7 +859,6 @@
                 break;
 
             case android.provider.Settings.System.SCREEN_BRIGHTNESS:
-            case android.provider.Settings.System.SCREEN_BRIGHTNESS_FLOAT:
             case android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE:
                 if (callingUid == Process.SYSTEM_UID) {
                     return false;
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 1a9e012..f7eb29f 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -141,6 +141,8 @@
     @NonNull
     private final PackageManagerService mPm;
 
+    private final int mInstallReason;
+
     VerifyingSession(UserHandle user, File stagedDir, IPackageInstallObserver2 observer,
             PackageInstaller.SessionParams sessionParams, InstallSource installSource,
             int installerUid, SigningDetails signingDetails, int sessionId, PackageLite lite,
@@ -168,6 +170,7 @@
         mUserActionRequiredType = sessionParams.requireUserAction;
         mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING;
         mIsStaged = sessionParams.isStaged;
+        mInstallReason = sessionParams.installReason;
     }
 
     @Override
@@ -190,7 +193,9 @@
         // Perform package verification and enable rollback (unless we are simply moving the
         // package).
         if (!mOriginInfo.mExisting) {
-            if (!isApex() && !isArchivedInstallation()) {
+            final boolean verifyForRollback = Flags.recoverabilityDetection()
+                    ? !isARollback() : true;
+            if (!isApex() && !isArchivedInstallation() && verifyForRollback) {
                 // TODO(b/182426975): treat APEX as APK when APK verification is concerned
                 sendApkVerificationRequest(pkgLite);
             }
@@ -200,6 +205,11 @@
         }
     }
 
+    private boolean isARollback() {
+        return mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK
+                && mInstallSource.mInitiatingPackageName.equals("android");
+    }
+
     private void sendApkVerificationRequest(PackageInfoLite pkgLite) {
         final int verificationId = mPm.mPendingVerificationToken++;
 
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 4ddd546..267ddd0 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.power.hint;
 
+import static android.os.Flags.adpfUseFmqChannel;
+
 import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.power.hint.Flags.powerhintThreadCleanup;
 
@@ -26,6 +28,8 @@
 import android.app.StatsManager;
 import android.app.UidObserver;
 import android.content.Context;
+import android.hardware.power.ChannelConfig;
+import android.hardware.power.IPower;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
 import android.hardware.power.WorkDuration;
@@ -39,6 +43,7 @@
 import android.os.PerformanceHintManager;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
@@ -69,6 +74,7 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -91,9 +97,21 @@
     @GuardedBy("mLock")
     private final ArrayMap<Integer, ArrayMap<IBinder, ArraySet<AppHintSession>>> mActiveSessions;
 
+    // Multi-level map storing all the channel binder token death listeners.
+    // First level is keyed by the UID of the client process owning the channel.
+    // Second level is the tgid of the process, which will often just be size one.
+    // Each channel is unique per (tgid, uid) pair, so this map associates each pair with an
+    // object that listens for the death notification of the binder token that was provided by
+    // that client when it created the channel, so we can detect when the client process dies.
+    @GuardedBy("mChannelMapLock")
+    private ArrayMap<Integer, TreeMap<Integer, ChannelItem>> mChannelMap;
+
     /** Lock to protect mActiveSessions and the UidObserver. */
     private final Object mLock = new Object();
 
+    /** Lock to protect mChannelMap. */
+    private final Object mChannelMapLock = new Object();
+
     @GuardedBy("mNonIsolatedTidsLock")
     private final Map<Integer, Set<Long>> mNonIsolatedTids;
 
@@ -110,6 +128,9 @@
 
     private AtomicBoolean mConfigCreationSupport = new AtomicBoolean(true);
 
+    private final IPower mPowerHal;
+    private int mPowerHalVersion;
+
     private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint";
     private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager";
 
@@ -131,12 +152,22 @@
             mNonIsolatedTids = null;
         }
         mActiveSessions = new ArrayMap<>();
+        mChannelMap = new ArrayMap<>();
         mNativeWrapper = injector.createNativeWrapper();
         mNativeWrapper.halInit();
         mHintSessionPreferredRate = mNativeWrapper.halGetHintSessionPreferredRate();
         mUidObserver = new MyUidObserver();
         mAmInternal = Objects.requireNonNull(
                 LocalServices.getService(ActivityManagerInternal.class));
+        mPowerHal = injector.createIPower();
+        mPowerHalVersion = 0;
+        if (mPowerHal != null) {
+            try {
+                mPowerHalVersion = mPowerHal.getInterfaceVersion();
+            } catch (RemoteException e) {
+                throw new IllegalStateException("Could not contact PowerHAL!", e);
+            }
+        }
     }
 
     private ServiceThread createCleanUpThread() {
@@ -151,6 +182,10 @@
         NativeWrapper createNativeWrapper() {
             return new NativeWrapper();
         }
+        IPower createIPower() {
+            return IPower.Stub.asInterface(
+                ServiceManager.waitForDeclaredService(IPower.DESCRIPTOR + "/default"));
+        }
     }
 
     private boolean isHalSupported() {
@@ -344,6 +379,16 @@
                         }
                     }
                 }
+                synchronized (mChannelMapLock) {
+                    // Clean up the uid's session channels
+                    final TreeMap<Integer, ChannelItem> uidMap = mChannelMap.get(uid);
+                    if (uidMap != null) {
+                        for (Map.Entry<Integer, ChannelItem> entry : uidMap.entrySet()) {
+                            entry.getValue().closeChannel();
+                        }
+                        mChannelMap.remove(uid);
+                    }
+                }
             });
         }
 
@@ -383,6 +428,113 @@
         }
     }
 
+    /**
+     * Creates a channel item in the channel map if one does not exist, then returns
+     * the entry in the channel map.
+     */
+    public ChannelItem getOrCreateMappedChannelItem(int tgid, int uid, IBinder token) {
+        synchronized (mChannelMapLock) {
+            if (!mChannelMap.containsKey(uid)) {
+                mChannelMap.put(uid, new TreeMap<Integer, ChannelItem>());
+            }
+            TreeMap<Integer, ChannelItem> map = mChannelMap.get(uid);
+            if (!map.containsKey(tgid)) {
+                ChannelItem item = new ChannelItem(tgid, uid, token);
+                item.openChannel();
+                map.put(tgid, item);
+            }
+            return map.get(tgid);
+        }
+    }
+
+    /**
+     * This removes an entry in the binder token callback map when a channel is closed,
+     * and unregisters its callbacks.
+     */
+    public void removeChannelItem(Integer tgid, Integer uid) {
+        synchronized (mChannelMapLock) {
+            TreeMap<Integer, ChannelItem> map = mChannelMap.get(uid);
+            if (map != null) {
+                ChannelItem item = map.get(tgid);
+                if (item != null) {
+                    item.closeChannel();
+                    map.remove(tgid);
+                }
+                if (map.isEmpty()) {
+                    mChannelMap.remove(uid);
+                }
+            }
+        }
+    }
+
+    /**
+     * Manages the lifecycle of a single channel. This includes caching the channel descriptor,
+     * receiving binder token death notifications, and handling cleanup on uid termination. There
+     * can only be one ChannelItem per (tgid, uid) pair in mChannelMap, and channel creation happens
+     * when a ChannelItem enters the map, while destruction happens when it leaves the map.
+     */
+    private class ChannelItem implements IBinder.DeathRecipient {
+        @Override
+        public void binderDied() {
+            removeChannelItem(mTgid, mUid);
+        }
+
+        ChannelItem(int tgid, int uid, IBinder token) {
+            this.mTgid = tgid;
+            this.mUid = uid;
+            this.mToken = token;
+            this.mLinked = false;
+            this.mConfig = null;
+        }
+
+        public void closeChannel() {
+            if (mLinked) {
+                mToken.unlinkToDeath(this, 0);
+                mLinked = false;
+            }
+            if (mConfig != null) {
+                try  {
+                    mPowerHal.closeSessionChannel(mTgid, mUid);
+                } catch (RemoteException e) {
+                    throw new IllegalStateException("Failed to close session channel!", e);
+                }
+                mConfig = null;
+            }
+        }
+
+        public void openChannel() {
+            if (!mLinked) {
+                try {
+                    mToken.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    throw new IllegalStateException("Client already dead", e);
+                }
+                mLinked = true;
+            }
+            if (mConfig == null) {
+                try {
+                    // This method uses PowerHAL directly through the SDK,
+                    // to avoid needing to pass the ChannelConfig through JNI.
+                    mConfig = mPowerHal.getSessionChannel(mTgid, mUid);
+                } catch (RemoteException e) {
+                    removeChannelItem(mTgid, mUid);
+                    throw new IllegalStateException("Failed to create session channel!", e);
+                }
+            }
+        }
+
+        ChannelConfig getConfig() {
+            return mConfig;
+        }
+
+        // To avoid accidental double-linking / unlinking
+        boolean mLinked;
+        final int mTgid;
+        final int mUid;
+        final IBinder mToken;
+        ChannelConfig mConfig;
+    }
+
     final class CleanUpHandler extends Handler {
         // status of processed tid used for caching
         private static final int TID_NOT_CHECKED = 0;
@@ -570,6 +722,18 @@
         return mService;
     }
 
+    @VisibleForTesting
+    Boolean hasChannel(int tgid, int uid) {
+        synchronized (mChannelMapLock) {
+            TreeMap<Integer, ChannelItem> uidMap = mChannelMap.get(uid);
+            if (uidMap != null) {
+                ChannelItem item = uidMap.get(tgid);
+                return item != null;
+            }
+            return false;
+        }
+    }
+
     // returns the first invalid tid or null if not found
     private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) {
         // Make sure all tids belongs to the same UID (including isolated UID),
@@ -710,6 +874,28 @@
         }
 
         @Override
+        public ChannelConfig getSessionChannel(IBinder token) {
+            if (mPowerHalVersion < 5 || !adpfUseFmqChannel()) {
+                return null;
+            }
+            java.util.Objects.requireNonNull(token);
+            final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
+            final int callingUid = Binder.getCallingUid();
+            ChannelItem item = getOrCreateMappedChannelItem(callingTgid, callingUid, token);
+            return item.getConfig();
+        };
+
+        @Override
+        public void closeSessionChannel() {
+            if (mPowerHalVersion < 5 || !adpfUseFmqChannel()) {
+                return;
+            }
+            final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid());
+            final int callingUid = Binder.getCallingUid();
+            removeChannelItem(callingTgid, callingUid);
+        };
+
+        @Override
         public long getHintSessionPreferredRate() {
             return mHintSessionPreferredRate;
         }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5e95a4b..237003a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -184,6 +184,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ConfigurationInfo;
+import android.content.pm.FeatureInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
@@ -266,6 +267,7 @@
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
+import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 import com.android.server.UiThread;
@@ -7400,7 +7402,25 @@
         }
     }
 
+    /** Cache the return value for {@link #isPip2ExperimentEnabled()} */
+    private static Boolean sIsPip2ExperimentEnabled = null;
+
+    /**
+     * @return {@code true} if PiP2 implementation should be used. Besides the trunk stable flag,
+     * system property can be used to override this read only flag during development.
+     * It's currently limited to phone form factor, i.e., not enabled on ARC / TV.
+     */
     static boolean isPip2ExperimentEnabled() {
-        return Flags.enablePip2Implementation();
+        if (sIsPip2ExperimentEnabled == null) {
+            final FeatureInfo arcFeature = SystemConfig.getInstance().getAvailableFeatures().get(
+                    "org.chromium.arc");
+            final FeatureInfo tvFeature = SystemConfig.getInstance().getAvailableFeatures().get(
+                    FEATURE_LEANBACK);
+            final boolean isArc = arcFeature != null && arcFeature.version >= 0;
+            final boolean isTv = tvFeature != null && tvFeature.version >= 0;
+            sIsPip2ExperimentEnabled = SystemProperties.getBoolean("wm_shell.pip2", false)
+                    || (Flags.enablePip2Implementation() && !isArc && !isTv);
+        }
+        return sIsPip2ExperimentEnabled;
     }
 }
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index 25885ed..e8faff6 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -96,6 +96,7 @@
     interface TransactionReadyListener {
         void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
         default void onTransactionCommitTimeout() {}
+        default void onReadyTimeout() {}
     }
 
     /**
@@ -410,6 +411,7 @@
             if (allFinished && !mReady) {
                 Slog.w(TAG, "Sync group " + mSyncId + " timed-out because not ready. If you see "
                         + "this, please file a bug.");
+                mListener.onReadyTimeout();
             }
             finishNow();
             removeFromDependencies(this);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 0e446b8..eb1f3b4 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -39,6 +39,7 @@
 import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel;
 import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
 import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg;
+import static com.android.window.flags.Flags.balImprovedMetrics;
 import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck;
 import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
 import static com.android.window.flags.Flags.balRequireOptInSameUid;
@@ -805,14 +806,25 @@
      * or {@link #BAL_BLOCK} if the launch should be blocked
      */
     BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
-        int callingUid = state.mCallingUid;
-        int callingPid = state.mCallingPid;
-        final String callingPackage = state.mCallingPackage;
-        WindowProcessController callerApp = state.mCallerApp;
+        // This is used to block background activity launch even if the app is still
+        // visible to user after user clicking home button.
+
+        // Normal apps with visible app window will be allowed to start activity if app switching
+        // is allowed, or apps like live wallpaper with non app visible window will be allowed.
+        final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
+                || state.mAppSwitchState == APP_SWITCH_FG_ONLY;
+        if (appSwitchAllowedOrFg && state.mCallingUidHasAnyVisibleWindow) {
+            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+                    /*background*/ false, "callingUid has visible window");
+        }
+        if (mService.mActiveUids.hasNonAppVisibleWindow(state.mCallingUid)) {
+            return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
+                    /*background*/ false, "callingUid has non-app visible window");
+        }
 
         // don't abort for the most important UIDs
-        final int callingAppId = UserHandle.getAppId(callingUid);
-        if (callingUid == Process.ROOT_UID
+        final int callingAppId = UserHandle.getAppId(state.mCallingUid);
+        if (state.mCallingUid == Process.ROOT_UID
                 || callingAppId == Process.SYSTEM_UID
                 || callingAppId == Process.NFC_UID) {
             return new BalVerdict(
@@ -821,7 +833,7 @@
         }
 
         // Always allow home application to start activities.
-        if (isHomeApp(callingUid, callingPackage)) {
+        if (isHomeApp(state.mCallingUid, state.mCallingPackage)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ false,
                     "Home app");
@@ -836,67 +848,46 @@
                     "Active ime");
         }
 
-        // This is used to block background activity launch even if the app is still
-        // visible to user after user clicking home button.
-        final int appSwitchState = mService.getBalAppSwitchesState();
-
-        // don't abort if the callingUid has a visible window or is a persistent system process
-        final int callingUidProcState = mService.mActiveUids.getUidState(callingUid);
-        final boolean callingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid);
-        final boolean isCallingUidPersistentSystemProcess =
-                callingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
-
-        // Normal apps with visible app window will be allowed to start activity if app switching
-        // is allowed, or apps like live wallpaper with non app visible window will be allowed.
-        final boolean appSwitchAllowedOrFg =
-                appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY;
-        if (appSwitchAllowedOrFg && callingUidHasAnyVisibleWindow) {
-            return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
-                    /*background*/ false, "callingUid has visible window");
-        }
-        if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) {
-            return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
-                    /*background*/ false, "callingUid has non-app visible window");
-        }
-
-        if (isCallingUidPersistentSystemProcess) {
+        // don't abort if the callingUid is a persistent system process
+        if (state.mIsCallingUidPersistentSystemProcess) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ false, "callingUid is persistent system process");
         }
 
         // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
-        if (hasBalPermission(callingUid, callingPid)) {
+        if (hasBalPermission(state.mCallingUid, state.mCallingPid)) {
             return new BalVerdict(BAL_ALLOW_PERMISSION,
                     /*background*/ true,
                     "START_ACTIVITIES_FROM_BACKGROUND permission granted");
         }
         // don't abort if the caller has the same uid as the recents component
-        if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
+        if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ true, "Recents Component");
         }
         // don't abort if the callingUid is the device owner
-        if (mService.isDeviceOwner(callingUid)) {
+        if (mService.isDeviceOwner(state.mCallingUid)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ true, "Device Owner");
         }
         // don't abort if the callingUid is a affiliated profile owner
-        if (mService.isAffiliatedProfileOwner(callingUid)) {
+        if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ true, "Affiliated Profile Owner");
         }
         // don't abort if the callingUid has companion device
-        final int callingUserId = UserHandle.getUserId(callingUid);
-        if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
+        final int callingUserId = UserHandle.getUserId(state.mCallingUid);
+        if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
             return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
                     /*background*/ true, "Companion App");
         }
         // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
-        if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
+        if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+                state.mCallingPackage)) {
             Slog.w(
                     TAG,
                     "Background activity start for "
-                            + callingPackage
+                            + state.mCallingPackage
                             + " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
             return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
                     /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -905,7 +896,7 @@
         // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
         if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
                 AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
-                callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) {
+                state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
             return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
                     "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
         }
@@ -914,7 +905,7 @@
         // That's the case for PendingIntent-based starts, since the creator's process might not be
         // up and alive.
         // Don't abort if the callerApp or other processes of that uid are allowed in any way.
-        BalVerdict callerAppAllowsBal = checkProcessAllowsBal(callerApp, state);
+        BalVerdict callerAppAllowsBal = checkProcessAllowsBal(state.mCallerApp, state);
         if (callerAppAllowsBal.allows()) {
             return callerAppAllowsBal;
         }
@@ -929,13 +920,6 @@
      */
     BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) {
 
-        if (state.isPendingIntentBalAllowedByPermission()
-                && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
-            return new BalVerdict(BAL_ALLOW_PERMISSION,
-                    /*background*/ false,
-                    "realCallingUid has BAL permission.");
-        }
-
         // Normal apps with visible app window will be allowed to start activity if app switching
         // is allowed, or apps like live wallpaper with non app visible window will be allowed.
         // The home app can start apps even if app switches are usually disallowed.
@@ -961,6 +945,13 @@
             }
         }
 
+        if (state.isPendingIntentBalAllowedByPermission()
+                && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
+            return new BalVerdict(BAL_ALLOW_PERMISSION,
+                    /*background*/ false,
+                    "realCallingUid has BAL permission.");
+        }
+
         // if the realCallingUid is a persistent system process, abort if the IntentSender
         // wasn't allowed to start an activity
         if (state.mForcedBalByPiSender.allowsBackgroundActivityStarts()
@@ -1660,28 +1651,63 @@
                             (state.mOriginatingPendingIntent != null));
         }
 
-        @BalCode int code = finalVerdict.getCode();
-        int callingUid = state.mCallingUid;
-        int realCallingUid = state.mRealCallingUid;
-        Intent intent = state.mIntent;
+        if (balImprovedMetrics()) {
+            if (shouldLogStats(finalVerdict, state)) {
+                String activityName;
+                if (shouldLogIntentActivity(finalVerdict, state)) {
+                    Intent intent = state.mIntent;
+                    activityName = intent == null ? "noIntent" // should never happen
+                            : requireNonNull(intent.getComponent()).flattenToShortString();
+                } else {
+                    activityName = "";
+                }
+                writeBalAllowedLog(activityName, finalVerdict.getCode(), state);
+            }
+        } else {
+            @BalCode int code = finalVerdict.getCode();
+            int callingUid = state.mCallingUid;
+            int realCallingUid = state.mRealCallingUid;
+            Intent intent = state.mIntent;
 
-        if (code == BAL_ALLOW_PENDING_INTENT
-                && (callingUid < Process.FIRST_APPLICATION_UID
-                || realCallingUid < Process.FIRST_APPLICATION_UID)) {
-            String activityName = intent != null
-                    ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
-            writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
-                    state);
-        }
-        if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
-                || code == BAL_ALLOW_SAW_PERMISSION) {
-            // We don't need to know which activity in this case.
-            writeBalAllowedLog("", code, state);
-
+            if (code == BAL_ALLOW_PENDING_INTENT
+                    && (callingUid < Process.FIRST_APPLICATION_UID
+                    || realCallingUid < Process.FIRST_APPLICATION_UID)) {
+                String activityName = intent != null
+                        ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
+                writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
+                        state);
+            }
+            if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
+                    || code == BAL_ALLOW_SAW_PERMISSION) {
+                // We don't need to know which activity in this case.
+                writeBalAllowedLog("", code, state);
+            }
         }
         return finalVerdict;
     }
 
+    @VisibleForTesting
+    boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
+        if (finalVerdict.blocks()) {
+            return false;
+        }
+        if (!state.isPendingIntent() && finalVerdict.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) {
+            return false;
+        }
+        if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()
+                && state.mResultForRealCaller.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) {
+            return false;
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
+        return finalVerdict.mBasedOnRealCaller
+                ? state.mRealCallingUid < Process.FIRST_APPLICATION_UID
+                : state.mCallingUid < Process.FIRST_APPLICATION_UID;
+    }
+
     @VisibleForTesting void writeBalAllowedLog(String activityName, int code, BalState state) {
         FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
                 activityName,
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4147249..c9a5e71 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -550,15 +550,6 @@
     // TODO(multi-display): remove some of the usages.
     boolean isDefaultDisplay;
 
-    /** Detect user tapping outside of current focused task bounds .*/
-    // TODO(b/315321016): Remove once pointer event detection is removed from WM.
-    @VisibleForTesting
-    final TaskTapPointerEventListener mTapDetector;
-
-    /** Detect user tapping outside of current focused root task bounds .*/
-    // TODO(b/315321016): Remove once pointer event detection is removed from WM.
-    private Region mTouchExcludeRegion = new Region();
-
     /** Save allocating when calculating rects */
     private final Rect mTmpRect = new Rect();
     private final Rect mTmpRect2 = new Rect();
@@ -571,10 +562,6 @@
 
     final PinnedTaskController mPinnedTaskController;
 
-    final ArrayList<WindowState> mTapExcludedWindows = new ArrayList<>();
-    /** A collection of windows that provide tap exclude regions inside of them. */
-    final ArraySet<WindowState> mTapExcludeProvidingWindows = new ArraySet<>();
-
     private final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList();
 
     private final TaskForResizePointSearchResult mTmpTaskForResizePointSearchResult =
@@ -1193,18 +1180,6 @@
                 "PointerEventDispatcher" + mDisplayId, mDisplayId);
         mPointerEventDispatcher = new PointerEventDispatcher(inputChannel);
 
-        if (com.android.input.flags.Flags.removePointerEventTrackingInWm()) {
-            mTapDetector = null;
-        } else {
-            // Tap Listeners are supported for:
-            // 1. All physical displays (multi-display).
-            // 2. VirtualDisplays on VR, AA (and everything else).
-            mTapDetector = new TaskTapPointerEventListener(mWmService, this);
-            registerPointerEventListener(mTapDetector);
-        }
-        if (mWmService.mMousePositionTracker != null) {
-            registerPointerEventListener(mWmService.mMousePositionTracker);
-        }
         if (mWmService.mAtmService.getRecentTasks() != null) {
             registerPointerEventListener(
                     mWmService.mAtmService.getRecentTasks().getInputListener());
@@ -3304,117 +3279,6 @@
                 mTmpTaskForResizePointSearchResult.process(taskDisplayArea, x, y, delta));
     }
 
-    void updateTouchExcludeRegion() {
-        if (mTapDetector == null) {
-            // The touch exclude region is used to detect the region outside of the focused task
-            // so that the tap detector can detect outside touches. Don't calculate the exclude
-            // region when the tap detector is disabled.
-            return;
-        }
-        final Task focusedTask = (mFocusedApp != null ? mFocusedApp.getTask() : null);
-        if (focusedTask == null) {
-            mTouchExcludeRegion.setEmpty();
-        } else {
-            mTouchExcludeRegion.set(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
-            final int delta = dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
-            mTmpRect.setEmpty();
-            mTmpRect2.setEmpty();
-
-            forAllTasks(t -> { processTaskForTouchExcludeRegion(t, focusedTask, delta); });
-
-            // If we removed the focused task above, add it back and only leave its
-            // outside touch area in the exclusion. TapDetector is not interested in
-            // any touch inside the focused task itself.
-            if (!mTmpRect2.isEmpty()) {
-                mTouchExcludeRegion.op(mTmpRect2, Region.Op.UNION);
-            }
-        }
-        if (mInputMethodWindow != null && mInputMethodWindow.isVisible()) {
-            // If the input method is visible and the user is typing, we don't want these touch
-            // events to be intercepted and used to change focus. This would likely cause a
-            // disappearance of the input method.
-            mInputMethodWindow.getTouchableRegion(mTmpRegion);
-            mTouchExcludeRegion.op(mTmpRegion, Op.UNION);
-        }
-        for (int i = mTapExcludedWindows.size() - 1; i >= 0; i--) {
-            final WindowState win = mTapExcludedWindows.get(i);
-            if (!win.isVisible()) {
-                continue;
-            }
-            win.getTouchableRegion(mTmpRegion);
-            mTouchExcludeRegion.op(mTmpRegion, Region.Op.UNION);
-        }
-        amendWindowTapExcludeRegion(mTouchExcludeRegion);
-        mTapDetector.setTouchExcludeRegion(mTouchExcludeRegion);
-    }
-
-    private void processTaskForTouchExcludeRegion(Task task, Task focusedTask, int delta) {
-        if (mTapDetector == null) {
-            // The touch exclude region is used to detect the region outside of the focused task
-            // so that the tap detector can detect outside touches. Don't calculate the exclude
-            // region when the tap detector is disabled.
-        }
-        final ActivityRecord topVisibleActivity = task.getTopVisibleActivity();
-
-        if (topVisibleActivity == null || !topVisibleActivity.hasContentToDisplay()) {
-            return;
-        }
-
-        // Exclusion region is the region that TapDetector doesn't care about.
-        // Here we want to remove all non-focused tasks from the exclusion region.
-        // We also remove the outside touch area for resizing for all freeform
-        // tasks (including the focused).
-        // We save the focused task region once we find it, and add it back at the end.
-        // If the task is root home task and it is resizable and visible (top of its root task),
-        // we want to exclude the root docked task from touch so we need the entire screen area
-        // and not just a small portion which the root home task currently is resized to.
-        if (task.isActivityTypeHome() && task.isVisible() && task.isResizeable()) {
-            task.getDisplayArea().getBounds(mTmpRect);
-        } else {
-            task.getDimBounds(mTmpRect);
-        }
-
-        if (task == focusedTask) {
-            // Add the focused task rect back into the exclude region once we are done
-            // processing root tasks.
-            // NOTE: this *looks* like a no-op, but this usage of mTmpRect2 is expected by
-            //       updateTouchExcludeRegion.
-            mTmpRect2.set(mTmpRect);
-        }
-
-        final boolean isFreeformed = task.inFreeformWindowingMode();
-        if (task != focusedTask || isFreeformed) {
-            if (isFreeformed) {
-                // If the task is freeformed, enlarge the area to account for outside
-                // touch area for resize.
-                mTmpRect.inset(-delta, -delta);
-                // Intersect with display content frame. If we have system decor (status bar/
-                // navigation bar), we want to exclude that from the tap detection.
-                // Otherwise, if the app is partially placed under some system button (eg.
-                // Recents, Home), pressing that button would cause a full series of
-                // unwanted transfer focus/resume/pause, before we could go home.
-                mTmpRect.inset(getInsetsStateController().getRawInsetsState().calculateInsets(
-                        mTmpRect, systemBars() | ime(), false /* ignoreVisibility */));
-            }
-            mTouchExcludeRegion.op(mTmpRect, Region.Op.DIFFERENCE);
-        }
-    }
-
-    /**
-     * Union the region with all the tap exclude region provided by windows on this display.
-     *
-     * @param inOutRegion The region to be amended.
-     */
-    private void amendWindowTapExcludeRegion(Region inOutRegion) {
-        final Region region = Region.obtain();
-        for (int i = mTapExcludeProvidingWindows.size() - 1; i >= 0; i--) {
-            final WindowState win = mTapExcludeProvidingWindows.valueAt(i);
-            win.getTapExcludeRegion(region);
-            inOutRegion.op(region, Op.UNION);
-        }
-        region.recycle();
-    }
-
     @Override
     void switchUser(int userId) {
         super.switchUser(userId);
@@ -3771,7 +3635,6 @@
         pw.print("x"); pw.println(mDisplayInfo.largestNominalAppHeight);
         pw.print(subPrefix + "deferred=" + mDeferredRemoval
                 + " mLayoutNeeded=" + mLayoutNeeded);
-        pw.println(" mTouchExcludeRegion=" + mTouchExcludeRegion);
 
         pw.println();
         super.dump(pw, prefix, dumpAll);
@@ -4120,7 +3983,6 @@
         }
 
         getInputMonitor().setFocusedAppLw(newFocus);
-        updateTouchExcludeRegion();
         return true;
     }
 
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 8116f68..30f2d0d 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -21,13 +21,11 @@
 import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
 import static android.view.View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG;
 
-import static com.android.input.flags.Flags.enablePointerChoreographer;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
 import android.annotation.NonNull;
-import android.app.ActivityManager;
 import android.content.ClipData;
 import android.content.Context;
 import android.hardware.input.InputManagerGlobal;
@@ -266,16 +264,12 @@
 
                 final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
                 mDragState.broadcastDragStartedLocked(touchX, touchY);
-                if (enablePointerChoreographer()) {
-                    if ((touchSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
-                        InputManagerGlobal.getInstance().setPointerIcon(
-                                PointerIcon.getSystemIcon(
-                                        mService.mContext, PointerIcon.TYPE_GRABBING),
-                                mDragState.mDisplayContent.getDisplayId(), touchDeviceId,
-                                touchPointerId, mDragState.getInputToken());
-                    }
-                } else {
-                    mDragState.overridePointerIconLocked(touchSource);
+                if ((touchSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
+                    InputManagerGlobal.getInstance().setPointerIcon(
+                            PointerIcon.getSystemIcon(
+                                    mService.mContext, PointerIcon.TYPE_GRABBING),
+                            mDragState.mDisplayContent.getDisplayId(), touchDeviceId,
+                            touchPointerId, mDragState.getInputToken());
                 }
                 // remember the thumb offsets for later
                 mDragState.mThumbOffsetX = thumbCenterX;
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 5ed343a..72ae64c 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -45,7 +45,6 @@
 import android.content.ClipDescription;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.hardware.input.InputManagerGlobal;
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
@@ -58,9 +57,7 @@
 import android.view.DragEvent;
 import android.view.InputApplicationHandle;
 import android.view.InputChannel;
-import android.view.InputDevice;
 import android.view.InputWindowHandle;
-import android.view.PointerIcon;
 import android.view.SurfaceControl;
 import android.view.View;
 import android.view.WindowManager;
@@ -110,7 +107,6 @@
     boolean mCrossProfileCopyAllowed;
     ClipData mData;
     ClipDescription mDataDescription;
-    int mTouchSource;
     boolean mDragResult;
     boolean mRelinquishDragSurfaceToDropTarget;
     float mAnimatedScale = 1.0f;
@@ -263,12 +259,6 @@
             Trace.instant(TRACE_TAG_WINDOW_MANAGER, "DragDropController#DRAG_ENDED");
         }
 
-        // Take the cursor back if it has been changed.
-        if (isFromSource(InputDevice.SOURCE_MOUSE)) {
-            mService.restorePointerIconLocked(mDisplayContent, mCurrentX, mCurrentY);
-            mTouchSource = 0;
-        }
-
         // Clear the internal variables.
         if (mInputSurface != null) {
             mTransaction.remove(mInputSurface).apply();
@@ -762,18 +752,6 @@
         return animator;
     }
 
-    private boolean isFromSource(int source) {
-        return (mTouchSource & source) == source;
-    }
-
-    void overridePointerIconLocked(int touchSource) {
-        mTouchSource = touchSource;
-        if (isFromSource(InputDevice.SOURCE_MOUSE)) {
-            // TODO(b/293587049): Pointer Icon Refactor: Set the pointer icon from the drag window.
-            InputManagerGlobal.getInstance().setPointerIconType(PointerIcon.TYPE_GRABBING);
-        }
-    }
-
     private class AnimationListener
             implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
         @Override
diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java
index 119fafd..ae6e724 100644
--- a/services/core/java/com/android/server/wm/InputConfigAdapter.java
+++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java
@@ -20,8 +20,6 @@
 import android.view.InputWindowHandle.InputConfigFlags;
 import android.view.WindowManager.LayoutParams;
 
-import java.util.List;
-
 /**
  * A helper to determine the {@link InputConfigFlags} that control the behavior of an input window
  * from several WM attributes.
@@ -47,7 +45,7 @@
      * input configurations that can be mapped directly from a corresponding LayoutParams input
      * feature.
      */
-    private static final List<FlagMapping> INPUT_FEATURE_TO_CONFIG_MAP = List.of(
+    private static final FlagMapping[] INPUT_FEATURE_TO_CONFIG_MAP = {
             new FlagMapping(
                     LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL,
                     InputConfig.NO_INPUT_CHANNEL, false /* inverted */),
@@ -59,7 +57,8 @@
                     InputConfig.SPY, false /* inverted */),
             new FlagMapping(
                     LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
-                    InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */));
+                    InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */)
+    };
 
     @InputConfigFlags
     private static final int INPUT_FEATURE_TO_CONFIG_MASK =
@@ -72,7 +71,7 @@
      * NOTE: The layout params flag {@link LayoutParams#FLAG_NOT_FOCUSABLE} is not handled by this
      * adapter, and must be handled explicitly.
      */
-    private static final List<FlagMapping> LAYOUT_PARAM_FLAG_TO_CONFIG_MAP = List.of(
+    private static final FlagMapping[] LAYOUT_PARAM_FLAG_TO_CONFIG_MAP = {
             new FlagMapping(
                     LayoutParams.FLAG_NOT_TOUCHABLE,
                     InputConfig.NOT_TOUCHABLE, false /* inverted */),
@@ -84,7 +83,8 @@
                     InputConfig.WATCH_OUTSIDE_TOUCH, false /* inverted */),
             new FlagMapping(
                     LayoutParams.FLAG_SLIPPERY,
-                    InputConfig.SLIPPERY, false /* inverted */));
+                    InputConfig.SLIPPERY, false /* inverted */)
+    };
 
     @InputConfigFlags
     private static final int LAYOUT_PARAM_FLAG_TO_CONFIG_MASK =
@@ -119,7 +119,7 @@
     }
 
     @InputConfigFlags
-    private static int applyMapping(int flags, List<FlagMapping> flagToConfigMap) {
+    private static int applyMapping(int flags, FlagMapping[] flagToConfigMap) {
         int inputConfig = 0;
         for (final FlagMapping mapping : flagToConfigMap) {
             final boolean flagSet = (flags & mapping.mFlag) != 0;
@@ -131,7 +131,7 @@
     }
 
     @InputConfigFlags
-    private static int computeMask(List<FlagMapping> flagToConfigMap) {
+    private static int computeMask(FlagMapping[] flagToConfigMap) {
         int mask = 0;
         for (final FlagMapping mapping : flagToConfigMap) {
             mask |= mapping.mInputConfig;
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index a84ebd9..22ca82a 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -290,22 +290,6 @@
         }
     }
 
-    @Override
-    public void notifyPointerDisplayIdChanged(int displayId, float x, float y) {
-        synchronized (mService.mGlobalLock) {
-            mService.setMousePointerDisplayId(displayId);
-            if (displayId == Display.INVALID_DISPLAY) return;
-
-            final DisplayContent dc = mService.mRoot.getDisplayContent(displayId);
-            if (dc == null) {
-                Slog.wtf(TAG, "The mouse pointer was moved to display " + displayId
-                        + " that does not have a valid DisplayContent.");
-                return;
-            }
-            mService.restorePointerIconLocked(dc, x, y);
-        }
-    }
-
     /** Waits until the built-in input devices have been configured. */
     public boolean waitForInputDevicesReady(long timeoutMillis) {
         synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 5aa0ed7..ce1a72d 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -25,7 +25,6 @@
 import android.content.Context;
 import android.graphics.Color;
 import android.provider.DeviceConfig;
-import android.util.Slog;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +32,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.function.Function;
+import java.util.function.IntSupplier;
 
 /** Reads letterbox configs from resources and controls their overrides at runtime. */
 final class LetterboxConfiguration {
@@ -265,6 +265,12 @@
     // unresizable apps
     private boolean mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox;
 
+    // Supplier for the value in pixel to consider when detecting vertical thin letterboxing
+    private final IntSupplier mThinLetterboxWidthFn;
+
+    // Supplier for the value in pixel to consider when detecting horizontal thin letterboxing
+    private final IntSupplier mThinLetterboxHeightFn;
+
     // Allows to enable letterboxing strategy for translucent activities ignoring flags.
     private boolean mTranslucentLetterboxingOverrideEnabled;
 
@@ -358,6 +364,10 @@
                 R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled);
         mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled);
+        mThinLetterboxWidthFn = () ->  mContext.getResources().getDimensionPixelSize(
+                R.dimen.config_letterboxThinLetterboxWidthDp);
+        mThinLetterboxHeightFn = () -> mContext.getResources().getDimensionPixelSize(
+                R.dimen.config_letterboxThinLetterboxHeightDp);
 
         mLetterboxConfigurationPersister = letterboxConfigurationPersister;
         mLetterboxConfigurationPersister.start();
@@ -1129,6 +1139,24 @@
     }
 
     /**
+     * @return Width in pixel about the padding to use to understand if the letterbox for an
+     *         activity is thin. If the available space has width W and the app has width w, this
+     *         is the maximum value for (W - w) / 2 to be considered for a thin letterboxed app.
+     */
+    int getThinLetterboxWidthPx() {
+        return mThinLetterboxWidthFn.getAsInt();
+    }
+
+    /**
+     * @return Height in pixel about the padding to use to understand if a letterbox is thin.
+     *         If the available space has height H and the app has height h, this is the maximum
+     *         value for (H - h) / 2 to be considered for a thin letterboxed app.
+     */
+    int getThinLetterboxHeightPx() {
+        return mThinLetterboxHeightFn.getAsInt();
+    }
+
+    /**
      * Overrides whether using split screen aspect ratio as a default aspect ratio for unresizable
      * apps.
      */
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index b38e666..9e16b8a 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -1024,6 +1024,67 @@
         return getSplitScreenAspectRatio();
     }
 
+    /**
+     * @return {@value true} if the resulting app is letterboxed in a way defined as thin.
+     */
+    boolean isVerticalThinLetterboxed() {
+        final int thinHeight = mLetterboxConfiguration.getThinLetterboxHeightPx();
+        if (thinHeight < 0) {
+            return false;
+        }
+        final Task task = mActivityRecord.getTask();
+        if (task == null) {
+            return false;
+        }
+        final int padding = Math.abs(
+                task.getBounds().height() - mActivityRecord.getBounds().height()) / 2;
+        return padding <= thinHeight;
+    }
+
+    /**
+     * @return {@value true} if the resulting app is pillarboxed in a way defined as thin.
+     */
+    boolean isHorizontalThinLetterboxed() {
+        final int thinWidth = mLetterboxConfiguration.getThinLetterboxWidthPx();
+        if (thinWidth < 0) {
+            return false;
+        }
+        final Task task = mActivityRecord.getTask();
+        if (task == null) {
+            return false;
+        }
+        final int padding = Math.abs(
+                task.getBounds().width() - mActivityRecord.getBounds().width()) / 2;
+        return padding <= thinWidth;
+    }
+
+
+    /**
+     * @return {@value true} if the vertical reachability should be allowed in case of
+     * thin letteboxing
+     */
+    boolean allowVerticalReachabilityForThinLetterbox() {
+        if (!Flags.disableThinLetterboxingReachability()) {
+            return true;
+        }
+        // When the flag is enabled we allow vertical reachability only if the
+        // app is not thin letterboxed vertically.
+        return !isVerticalThinLetterboxed();
+    }
+
+    /**
+     * @return {@value true} if the vertical reachability should be enabled in case of
+     * thin letteboxing
+     */
+    boolean allowHorizontalReachabilityForThinLetterbox() {
+        if (!Flags.disableThinLetterboxingReachability()) {
+            return true;
+        }
+        // When the flag is enabled we allow horizontal reachability only if the
+        // app is not thin pillarboxed.
+        return !isHorizontalThinLetterboxed();
+    }
+
     float getSplitScreenAspectRatio() {
         // Getting the same aspect ratio that apps get in split screen.
         final DisplayArea displayArea = mActivityRecord.getDisplayArea();
@@ -1263,6 +1324,9 @@
      * </ul>
      */
     private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) {
+        if (!allowHorizontalReachabilityForThinLetterbox()) {
+            return false;
+        }
         // Use screen resolved bounds which uses resolved bounds or size compat bounds
         // as activity bounds can sometimes be empty
         final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
@@ -1298,6 +1362,9 @@
      * </ul>
      */
     private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) {
+        if (!allowVerticalReachabilityForThinLetterbox()) {
+            return false;
+        }
         // Use screen resolved bounds which uses resolved bounds or size compat bounds
         // as activity bounds can sometimes be empty
         final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior()
@@ -1566,6 +1633,8 @@
         if (!shouldShowLetterboxUi) {
             return;
         }
+        pw.println(prefix + "  isVerticalThinLetterboxed=" + isVerticalThinLetterboxed());
+        pw.println(prefix + "  isHorizontalThinLetterboxed=" + isHorizontalThinLetterboxed());
         pw.println(prefix + "  letterboxBackgroundColor=" + Integer.toHexString(
                 getLetterboxBackgroundColor().toArgb()));
         pw.println(prefix + "  letterboxBackgroundType="
diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS
index ce47f5c..60454fc 100644
--- a/services/core/java/com/android/server/wm/OWNERS
+++ b/services/core/java/com/android/server/wm/OWNERS
@@ -18,6 +18,7 @@
 yunfanc@google.com
 wilsonshih@google.com
 jiamingliu@google.com
+pdwilliams@google.com
 
 # Files related to background activity launches
 per-file Background*Start* = set noparent
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 6003c1b..be8c2ae 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -911,7 +911,6 @@
             dc.getInputMonitor().updateInputWindowsLw(true /*force*/);
             dc.updateSystemGestureExclusion();
             dc.updateKeepClearAreas();
-            dc.updateTouchExcludeRegion();
         });
 
         // Check to see if we are now in a state where the screen should
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index bb86460..3b3eeb4 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -740,16 +740,6 @@
     }
 
     @Override
-    public void updatePointerIcon(IWindow window) {
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            mService.updatePointerIcon(window);
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
-    }
-
-    @Override
     public void updateTapExcludeRegion(IWindow window, Region region) {
         final long identity = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a9c47b8..8bd7b5f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -20,7 +20,6 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
 import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
-import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
 import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
@@ -171,7 +170,6 @@
 import android.view.DisplayInfo;
 import android.view.InsetsState;
 import android.view.RemoteAnimationAdapter;
-import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.view.WindowManager.TransitionOldType;
@@ -362,6 +360,10 @@
      * user wants to return to it. */
     private WindowProcessController mRootProcess;
 
+    /** The TF host info are set once the task has ever added an organized task fragment. */
+    int mTaskFragmentHostUid;
+    String mTaskFragmentHostProcessName;
+
     /** Takes on same value as first root activity */
     boolean isPersistable = false;
     int maxRecents;
@@ -438,16 +440,6 @@
     // Id of the previous display the root task was on.
     int mPrevDisplayId = INVALID_DISPLAY;
 
-    /** ID of the display which rotation {@link #mRotation} has. */
-    private int mLastRotationDisplayId = INVALID_DISPLAY;
-
-    /**
-     * Display rotation as of the last time {@link #setBounds(Rect)} was called or this task was
-     * moved to a new display.
-     */
-    @Surface.Rotation
-    private int mRotation;
-
     int mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
 
     /**
@@ -458,10 +450,7 @@
      */
     int mLastReportedRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 
-    // For comparison with DisplayContent bounds.
-    private Rect mTmpRect = new Rect();
-    // For handling display rotations.
-    private Rect mTmpRect2 = new Rect();
+    private final Rect mTmpRect = new Rect();
 
     // Resize mode of the task. See {@link ActivityInfo#resizeMode}
     // Based on the {@link ActivityInfo#resizeMode} of the root activity.
@@ -1194,9 +1183,6 @@
             updateOverrideConfigurationFromLaunchBounds();
         }
 
-        // Update task bounds if needed.
-        adjustBoundsForDisplayChangeIfNeeded(getDisplayContent());
-
         mRootWindowContainer.updateUIDsPresentOnDisplay();
 
         // Ensure all animations are finished at same time in split-screen mode.
@@ -1468,6 +1454,11 @@
         // passed from Task constructor.
         final TaskFragment childTaskFrag = child.asTaskFragment();
         if (childTaskFrag != null && childTaskFrag.asTask() == null) {
+            if (childTaskFrag.mTaskFragmentOrganizerProcessName != null
+                    && mTaskFragmentHostProcessName == null) {
+                mTaskFragmentHostUid = childTaskFrag.mTaskFragmentOrganizerUid;
+                mTaskFragmentHostProcessName = childTaskFrag.mTaskFragmentOrganizerProcessName;
+            }
             childTaskFrag.setMinDimensions(mMinWidth, mMinHeight);
 
             // The starting window should keep covering its task when a pure TaskFragment is added
@@ -2731,15 +2722,7 @@
             return setBounds(getRequestedOverrideBounds(), bounds);
         }
 
-        int rotation = Surface.ROTATION_0;
-        final DisplayContent displayContent = getRootTask() != null
-                ? getRootTask().getDisplayContent() : null;
-        if (displayContent != null) {
-            rotation = displayContent.getDisplayInfo().rotation;
-        }
-
         final int boundsChange = super.setBounds(bounds);
-        mRotation = rotation;
         updateSurfacePositionNonOrganized();
         return boundsChange;
     }
@@ -2799,10 +2782,6 @@
 
     @Override
     void onDisplayChanged(DisplayContent dc) {
-        final boolean isRootTask = isRootTask();
-        if (!isRootTask && !mCreatedByOrganizer) {
-            adjustBoundsForDisplayChangeIfNeeded(dc);
-        }
         super.onDisplayChanged(dc);
         if (isLeafTask()) {
             final int displayId = (dc != null) ? dc.getDisplayId() : INVALID_DISPLAY;
@@ -2953,48 +2932,6 @@
         return mDragResizing;
     }
 
-    void adjustBoundsForDisplayChangeIfNeeded(final DisplayContent displayContent) {
-        if (displayContent == null) {
-            return;
-        }
-        if (getRequestedOverrideBounds().isEmpty()) {
-            return;
-        }
-        final int displayId = displayContent.getDisplayId();
-        final int newRotation = displayContent.getDisplayInfo().rotation;
-        if (displayId != mLastRotationDisplayId) {
-            // This task is on a display that it wasn't on. There is no point to keep the relative
-            // position if display rotations for old and new displays are different. Just keep these
-            // values.
-            mLastRotationDisplayId = displayId;
-            mRotation = newRotation;
-            return;
-        }
-
-        if (mRotation == newRotation) {
-            // Rotation didn't change. We don't need to adjust the bounds to keep the relative
-            // position.
-            return;
-        }
-
-        // Device rotation changed.
-        // - We don't want the task to move around on the screen when this happens, so update the
-        //   task bounds so it stays in the same place.
-        // - Rotate the bounds and notify activity manager if the task can be resized independently
-        //   from its root task. The root task will take care of task rotation for the other case.
-        mTmpRect2.set(getBounds());
-
-        if (!getWindowConfiguration().canResizeTask()) {
-            setBounds(mTmpRect2);
-            return;
-        }
-
-        displayContent.rotateBounds(mRotation, newRotation, mTmpRect2);
-        if (setBounds(mTmpRect2) != BOUNDS_CHANGE_NONE) {
-            mAtmService.resizeTask(mTaskId, getBounds(), RESIZE_MODE_SYSTEM_SCREEN_ROTATION);
-        }
-    }
-
     /** Cancels any running app transitions associated with the task. */
     void cancelTaskWindowTransition() {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
@@ -3525,8 +3462,6 @@
         info.isVisibleRequested = isVisibleRequested();
         info.isSleeping = shouldSleepActivities();
         info.isTopActivityTransparent = top != null && !top.fillsParent();
-        appCompatTaskInfo.isLetterboxDoubleTapEnabled = top != null
-                && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled();
         appCompatTaskInfo.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
         appCompatTaskInfo.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
         appCompatTaskInfo.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET;
@@ -3541,15 +3476,29 @@
             appCompatTaskInfo.topActivityLetterboxWidth = top.getBounds().width();
             appCompatTaskInfo.topActivityLetterboxHeight = top.getBounds().height();
         }
+        // We need to consider if letterboxed or pillarboxed
+        // TODO(b/336807329) Encapsulate reachability logic
+        appCompatTaskInfo.isLetterboxDoubleTapEnabled = top != null
+                && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled();
         if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) {
             if (appCompatTaskInfo.isTopActivityPillarboxed()) {
-                // Pillarboxed
-                appCompatTaskInfo.topActivityLetterboxHorizontalPosition =
-                        top.mLetterboxUiController.getLetterboxPositionForHorizontalReachability();
+                if (top.mLetterboxUiController.allowHorizontalReachabilityForThinLetterbox()) {
+                    // Pillarboxed
+                    appCompatTaskInfo.topActivityLetterboxHorizontalPosition =
+                            top.mLetterboxUiController
+                                    .getLetterboxPositionForHorizontalReachability();
+                } else {
+                    appCompatTaskInfo.isLetterboxDoubleTapEnabled = false;
+                }
             } else {
-                // Letterboxed
-                appCompatTaskInfo.topActivityLetterboxVerticalPosition =
-                        top.mLetterboxUiController.getLetterboxPositionForVerticalReachability();
+                if (top.mLetterboxUiController.allowVerticalReachabilityForThinLetterbox()) {
+                    // Letterboxed
+                    appCompatTaskInfo.topActivityLetterboxVerticalPosition =
+                            top.mLetterboxUiController
+                                    .getLetterboxPositionForVerticalReachability();
+                } else {
+                    appCompatTaskInfo.isLetterboxDoubleTapEnabled = false;
+                }
             }
         }
         appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton = top != null
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 2b631f7..6a7f60b 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -320,9 +320,9 @@
     /** Organizer that organizing this TaskFragment. */
     @Nullable
     private ITaskFragmentOrganizer mTaskFragmentOrganizer;
-    @VisibleForTesting
+
     int mTaskFragmentOrganizerUid = INVALID_UID;
-    private @Nullable String mTaskFragmentOrganizerProcessName;
+    @Nullable String mTaskFragmentOrganizerProcessName;
 
     /** Client assigned unique token for this TaskFragment if this is created by an organizer. */
     @Nullable
@@ -485,14 +485,16 @@
      */
     @Nullable
     private WindowProcessController getOrganizerProcessIfDifferent(@Nullable ActivityRecord r) {
-        if ((r == null || mTaskFragmentOrganizerProcessName == null)
-                || (mTaskFragmentOrganizerProcessName.equals(r.processName)
-                && mTaskFragmentOrganizerUid == r.getUid())) {
-            // No organizer or the process is the same.
+        final Task task = getTask();
+        if (r == null || task == null || task.mTaskFragmentHostProcessName == null) {
             return null;
         }
-        return mAtmService.getProcessController(mTaskFragmentOrganizerProcessName,
-                mTaskFragmentOrganizerUid);
+        if (task.mTaskFragmentHostProcessName.equals(r.processName)
+                && task.mTaskFragmentHostUid == r.getUid()) {
+            return null;
+        }
+        return mAtmService.getProcessController(task.mTaskFragmentHostProcessName,
+                task.mTaskFragmentHostUid);
     }
 
     void setAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
diff --git a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java b/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
deleted file mode 100644
index ac244c7..0000000
--- a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_NOT_SPECIFIED;
-import static android.view.PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
-
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.hardware.input.InputManagerGlobal;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.WindowManagerPolicyConstants.PointerEventListener;
-
-import com.android.server.wm.WindowManagerService.H;
-
-/**
- * 1. Adjust the top most focus display if touch down on some display.
- * 2. Adjust the pointer icon when cursor moves to the task bounds.
- */
-public class TaskTapPointerEventListener implements PointerEventListener {
-
-    private final Region mTouchExcludeRegion = new Region();
-    private final WindowManagerService mService;
-    private final DisplayContent mDisplayContent;
-    private final Rect mTmpRect = new Rect();
-    private int mPointerIconType = TYPE_NOT_SPECIFIED;
-
-    public TaskTapPointerEventListener(WindowManagerService service,
-            DisplayContent displayContent) {
-        // TODO(b/315321016): Remove this class when the flag rollout is complete.
-        if (com.android.input.flags.Flags.removePointerEventTrackingInWm()) {
-            throw new IllegalStateException("TaskTapPointerEventListener should not be used!");
-        }
-        mService = service;
-        mDisplayContent = displayContent;
-    }
-
-    private void restorePointerIcon(int x, int y) {
-        if (mPointerIconType != TYPE_NOT_SPECIFIED) {
-            mPointerIconType = TYPE_NOT_SPECIFIED;
-            // Find the underlying window and ask it to restore the pointer icon.
-            mService.mH.removeMessages(H.RESTORE_POINTER_ICON);
-            mService.mH.obtainMessage(H.RESTORE_POINTER_ICON,
-                    x, y, mDisplayContent).sendToTarget();
-        }
-    }
-
-    @Override
-    public void onPointerEvent(MotionEvent motionEvent) {
-        switch (motionEvent.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN: {
-                final int x;
-                final int y;
-                if (motionEvent.getSource() == InputDevice.SOURCE_MOUSE) {
-                    x = (int) motionEvent.getXCursorPosition();
-                    y = (int) motionEvent.getYCursorPosition();
-                } else {
-                    x = (int) motionEvent.getX();
-                    y = (int) motionEvent.getY();
-                }
-
-                synchronized (this) {
-                    if (!mTouchExcludeRegion.contains(x, y)) {
-                        mService.mTaskPositioningController.handleTapOutsideTask(
-                                mDisplayContent, x, y);
-                    }
-                }
-            }
-            break;
-            case MotionEvent.ACTION_HOVER_ENTER:
-            case MotionEvent.ACTION_HOVER_MOVE: {
-                final int x = (int) motionEvent.getX();
-                final int y = (int) motionEvent.getY();
-                if (mTouchExcludeRegion.contains(x, y)) {
-                    restorePointerIcon(x, y);
-                    break;
-                }
-                final Task task = mDisplayContent.findTaskForResizePoint(x, y);
-                int iconType = TYPE_NOT_SPECIFIED;
-                if (task != null) {
-                    task.getDimBounds(mTmpRect);
-                    if (!mTmpRect.isEmpty() && !mTmpRect.contains(x, y)) {
-                        if (x < mTmpRect.left) {
-                            iconType =
-                                (y < mTmpRect.top) ? TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW :
-                                (y > mTmpRect.bottom) ? TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW :
-                                TYPE_HORIZONTAL_DOUBLE_ARROW;
-                        } else if (x > mTmpRect.right) {
-                            iconType =
-                                (y < mTmpRect.top) ? TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW :
-                                (y > mTmpRect.bottom) ? TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW :
-                                TYPE_HORIZONTAL_DOUBLE_ARROW;
-                        } else if (y < mTmpRect.top || y > mTmpRect.bottom) {
-                            iconType = TYPE_VERTICAL_DOUBLE_ARROW;
-                        }
-                    }
-                }
-                if (mPointerIconType != iconType) {
-                    mPointerIconType = iconType;
-                    if (mPointerIconType == TYPE_NOT_SPECIFIED) {
-                        // Find the underlying window and ask it restore the pointer icon.
-                        mService.mH.removeMessages(H.RESTORE_POINTER_ICON);
-                        mService.mH.obtainMessage(H.RESTORE_POINTER_ICON,
-                                x, y, mDisplayContent).sendToTarget();
-                    } else {
-                        InputManagerGlobal.getInstance()
-                                .setPointerIconType(mPointerIconType);
-                    }
-                }
-            }
-            break;
-            case MotionEvent.ACTION_HOVER_EXIT: {
-                final int x = (int) motionEvent.getX();
-                final int y = (int) motionEvent.getY();
-                restorePointerIcon(x, y);
-            }
-            break;
-        }
-    }
-
-    void setTouchExcludeRegion(Region newRegion) {
-        synchronized (this) {
-           mTouchExcludeRegion.set(newRegion);
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 1543263..7ec31d5 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1648,14 +1648,6 @@
         }
 
         if (mController.useFullReadyTracking()) {
-            if (mReadyTracker.mMet.isEmpty()) {
-                Slog.e(TAG, "#" + mSyncId + ": No conditions provided");
-            } else {
-                for (int i = 0; i < mReadyTracker.mConditions.size(); ++i) {
-                    Slog.e(TAG, "#" + mSyncId + ": unmet condition at ready: "
-                            + mReadyTracker.mConditions.get(i));
-                }
-            }
             for (int i = 0; i < mReadyTracker.mMet.size(); ++i) {
                 ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "#%d: Met condition: %s",
                         mSyncId, mReadyTracker.mMet.get(i));
@@ -3360,6 +3352,18 @@
         applyReady();
     }
 
+    @Override
+    public void onReadyTimeout() {
+        if (!mController.useFullReadyTracking()) {
+            Slog.e(TAG, "#" + mSyncId + " readiness timeout, used=" + mReadyTrackerOld.mUsed
+                    + " deferReadyDepth=" + mReadyTrackerOld.mDeferReadyDepth
+                    + " group=" + mReadyTrackerOld.mReadyGroups);
+            return;
+        }
+        Slog.e(TAG, "#" + mSyncId + " met conditions: " + mReadyTracker.mMet);
+        Slog.e(TAG, "#" + mSyncId + " unmet conditions: " + mReadyTracker.mConditions);
+    }
+
     /**
      * Represents a condition that must be met before an associated transition can be considered
      * ready.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index dbe3d36..e02e5be 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -81,15 +81,11 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
 import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
 import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
 import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
-import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
-import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
 import static android.view.WindowManager.TRANSIT_NONE;
@@ -203,7 +199,6 @@
 import android.hardware.configstore.V1_1.ISurfaceFlingerConfigs;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
-import android.hardware.input.InputManager;
 import android.hardware.input.InputSettings;
 import android.net.Uri;
 import android.os.Binder;
@@ -289,8 +284,6 @@
 import android.view.InsetsState;
 import android.view.KeyEvent;
 import android.view.MagnificationSpec;
-import android.view.MotionEvent;
-import android.view.PointerIcon;
 import android.view.RemoteAnimationAdapter;
 import android.view.ScrollCaptureResponse;
 import android.view.Surface;
@@ -335,6 +328,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IResultReceiver;
+import com.android.internal.os.TransferPipe;
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.policy.IKeyguardLockedStateListener;
 import com.android.internal.policy.IShortcutService;
@@ -545,13 +539,16 @@
             if (asProto) {
                 return;
             }
+
+            final long timeoutMs = 1000L;
             mAtmService.dumpActivity(fd, pw, /* name= */ "all", /* args= */ new String[]{},
                     /* opti= */ 0,
                     /* dumpAll= */ true,
                     /* dumpVisibleRootTasksOnly= */ true,
                     /* dumpFocusedRootTaskOnly= */ false, INVALID_DISPLAY, UserHandle.USER_ALL,
-                    /* timeout= */ 1000
+                    timeoutMs
             );
+            dumpVisibleWindowClients(fd, pw, timeoutMs);
         }
 
         @Override
@@ -1523,18 +1520,6 @@
         }
     }
 
-    static boolean excludeWindowTypeFromTapOutTask(int windowType) {
-        switch (windowType) {
-            case TYPE_STATUS_BAR:
-            case TYPE_NOTIFICATION_SHADE:
-            case TYPE_NAVIGATION_BAR:
-            case TYPE_INPUT_METHOD_DIALOG:
-            case TYPE_VOLUME_OVERLAY:
-                return true;
-        }
-        return false;
-    }
-
     public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
             int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
             InputChannel outInputChannel, InsetsState outInsetsState,
@@ -1833,10 +1818,6 @@
                 displayContent.mWinAddedSinceNullFocus.add(win);
             }
 
-            if (excludeWindowTypeFromTapOutTask(type)) {
-                displayContent.mTapExcludedWindows.add(win);
-            }
-
             win.mSession.onWindowAdded(win);
             mWindowMap.put(client.asBinder(), win);
             win.initAppOpsState();
@@ -5716,7 +5697,6 @@
 
         public static final int UPDATE_ANIMATION_SCALE = 51;
         public static final int WINDOW_HIDE_TIMEOUT = 52;
-        public static final int RESTORE_POINTER_ICON = 55;
         public static final int SET_HAS_OVERLAY_UI = 58;
         public static final int ANIMATION_FAILSAFE = 60;
         public static final int RECOMPUTE_FOCUS = 61;
@@ -5949,12 +5929,6 @@
                     }
                     break;
                 }
-                case RESTORE_POINTER_ICON: {
-                    synchronized (mGlobalLock) {
-                        restorePointerIconLocked((DisplayContent)msg.obj, msg.arg1, msg.arg2);
-                    }
-                    break;
-                }
                 case SET_HAS_OVERLAY_UI: {
                     mAmInternal.setHasOverlayUi(msg.arg1, msg.arg2 == 1);
                     break;
@@ -7574,144 +7548,6 @@
         }
     }
 
-    // The mouse position tracker will be obsolete after the Pointer Icon Refactor.
-    // TODO(b/293587049): Remove after the refactoring is fully rolled out.
-    @Nullable
-    final MousePositionTracker mMousePositionTracker =
-            com.android.input.flags.Flags.enablePointerChoreographer() ? null
-                    : new MousePositionTracker();
-
-    private static class MousePositionTracker implements PointerEventListener {
-        private boolean mLatestEventWasMouse;
-        private float mLatestMouseX;
-        private float mLatestMouseY;
-
-        /**
-         * The display that the pointer (mouse cursor) is currently shown on. This is updated
-         * directly by InputManagerService when the pointer display changes.
-         */
-        private int mPointerDisplayId = INVALID_DISPLAY;
-
-        /**
-         * Update the mouse cursor position as a result of a mouse movement.
-         * @return true if the position was successfully updated, false otherwise.
-         */
-        boolean updatePosition(int displayId, float x, float y) {
-            synchronized (this) {
-                mLatestEventWasMouse = true;
-
-                if (displayId != mPointerDisplayId) {
-                    // The display of the position update does not match the display on which the
-                    // mouse pointer is shown, so do not update the position.
-                    return false;
-                }
-                mLatestMouseX = x;
-                mLatestMouseY = y;
-                return true;
-            }
-        }
-
-        void setPointerDisplayId(int displayId) {
-            synchronized (this) {
-                mPointerDisplayId = displayId;
-            }
-        }
-
-        @Override
-        public void onPointerEvent(MotionEvent motionEvent) {
-            if (motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
-                updatePosition(motionEvent.getDisplayId(), motionEvent.getRawX(),
-                        motionEvent.getRawY());
-            } else {
-                synchronized (this) {
-                    mLatestEventWasMouse = false;
-                }
-            }
-        }
-    };
-
-    void updatePointerIcon(IWindow client) {
-        if (mMousePositionTracker == null) {
-            return;
-        }
-        int pointerDisplayId;
-        float mouseX, mouseY;
-
-        synchronized(mMousePositionTracker) {
-            if (!mMousePositionTracker.mLatestEventWasMouse) {
-                return;
-            }
-            mouseX = mMousePositionTracker.mLatestMouseX;
-            mouseY = mMousePositionTracker.mLatestMouseY;
-            pointerDisplayId = mMousePositionTracker.mPointerDisplayId;
-        }
-
-        synchronized (mGlobalLock) {
-            if (mDragDropController.dragDropActiveLocked()) {
-                // Drag cursor overrides the app cursor.
-                return;
-            }
-            WindowState callingWin = windowForClientLocked(null, client, false);
-            if (callingWin == null) {
-                ProtoLog.w(WM_ERROR, "Bad requesting window %s", client);
-                return;
-            }
-            final DisplayContent displayContent = callingWin.getDisplayContent();
-            if (displayContent == null) {
-                return;
-            }
-            if (pointerDisplayId != displayContent.getDisplayId()) {
-                // Do not let the pointer icon be updated by a window on a different display.
-                return;
-            }
-            WindowState windowUnderPointer =
-                    displayContent.getTouchableWinAtPointLocked(mouseX, mouseY);
-            if (windowUnderPointer != callingWin) {
-                return;
-            }
-            try {
-                windowUnderPointer.mClient.updatePointerIcon(
-                        windowUnderPointer.translateToWindowX(mouseX),
-                        windowUnderPointer.translateToWindowY(mouseY));
-            } catch (RemoteException e) {
-                ProtoLog.w(WM_ERROR, "unable to update pointer icon");
-            }
-        }
-    }
-
-    void restorePointerIconLocked(DisplayContent displayContent, float latestX, float latestY) {
-        if (mMousePositionTracker == null) {
-            return;
-        }
-        // Mouse position tracker has not been getting updates while dragging, update it now.
-        if (!mMousePositionTracker.updatePosition(
-                displayContent.getDisplayId(), latestX, latestY)) {
-            // The mouse position could not be updated, so ignore this request.
-            return;
-        }
-
-        WindowState windowUnderPointer =
-                displayContent.getTouchableWinAtPointLocked(latestX, latestY);
-        if (windowUnderPointer != null) {
-            try {
-                windowUnderPointer.mClient.updatePointerIcon(
-                        windowUnderPointer.translateToWindowX(latestX),
-                        windowUnderPointer.translateToWindowY(latestY));
-            } catch (RemoteException e) {
-                ProtoLog.w(WM_ERROR, "unable to restore pointer icon");
-            }
-        } else {
-            mContext.getSystemService(InputManager.class)
-                    .setPointerIconType(PointerIcon.TYPE_DEFAULT);
-        }
-    }
-    void setMousePointerDisplayId(int displayId) {
-        if (mMousePositionTracker == null) {
-            return;
-        }
-        mMousePositionTracker.setPointerDisplayId(displayId);
-    }
-
     /**
      * Update a tap exclude region in the window identified by the provided id. Touches down on this
      * region will not:
@@ -10350,4 +10186,32 @@
         }
         return true;
     }
+
+    /**
+     * Dump ViewRootImpl for visible non-activity windows.
+     */
+    private void dumpVisibleWindowClients(FileDescriptor fd, PrintWriter pw, long timeout) {
+        final ArrayList<WindowState> systemWindows = new ArrayList<>();
+        synchronized (mGlobalLock) {
+            mRoot.forAllWindows(w -> {
+                if (!w.isActivityWindow() && w.isVisibleNow()) {
+                    systemWindows.add(w);
+                }
+            }, false /* traverseTopToBottom */);
+        }
+
+        systemWindows.forEach(w -> {
+            pw.println("---------------------------------");
+            pw.println(w.toString());
+            pw.flush();
+            try (TransferPipe tp = new TransferPipe()) {
+                w.mClient.dumpWindow(tp.getWriteFd());
+                tp.go(fd, timeout);
+            } catch (IOException e) {
+                pw.println("Failure while dumping the window: " + e);
+            } catch (RemoteException e) {
+                pw.println("Got a RemoteException while dumping the window");
+            }
+        });
+    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index c25080f..dddc7b1 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -858,6 +858,10 @@
      * {@link InsetsStateController#notifyInsetsChanged}.
      */
     boolean isReadyToDispatchInsetsState() {
+        if (mStartingData != null) {
+            // Starting window doesn't consider insets.
+            return false;
+        }
         final boolean visible = shouldCheckTokenVisibleRequested()
                 ? isVisibleRequested() : isVisible();
         return visible && mFrozenInsetsState == null;
@@ -2355,18 +2359,12 @@
         }
 
         final int type = mAttrs.type;
-        if (WindowManagerService.excludeWindowTypeFromTapOutTask(type)) {
-            dc.mTapExcludedWindows.remove(this);
-        }
 
         if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) {
             mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(),
                     /*isShown=*/ false);
         }
 
-        // Remove this window from mTapExcludeProvidingWindows. If it was not registered, this will
-        // not do anything.
-        dc.mTapExcludeProvidingWindows.remove(this);
         dc.getDisplayPolicy().removeWindowLw(this);
 
         disposeInputChannel();
@@ -5522,18 +5520,10 @@
         // Clear the tap excluded region if the region passed in is null or empty.
         if (region == null || region.isEmpty()) {
             mTapExcludeRegion.setEmpty();
-            // Remove this window from mTapExcludeProvidingWindows since it won't be providing
-            // tap exclude regions.
-            currentDisplay.mTapExcludeProvidingWindows.remove(this);
         } else {
             mTapExcludeRegion.set(region);
-            // Make sure that this window is registered as one that provides a tap exclude region
-            // for its containing display.
-            currentDisplay.mTapExcludeProvidingWindows.add(this);
         }
 
-        // Trigger touch exclude region update on current display.
-        currentDisplay.updateTouchExcludeRegion();
         // Trigger touchable region update for this window.
         currentDisplay.getInputMonitor().updateInputWindowsLw(true /* force */);
     }
@@ -6070,6 +6060,10 @@
         return mPrepareSyncSeqId > 0;
     }
 
+    public boolean isActivityWindow() {
+        return mActivityRecord != null;
+    }
+
     void setSecureLocked(boolean isSecure) {
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isSecure=%b: %s", isSecure, getName());
         if (secureWindowState()) {
diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp
index e65b903..22c0f73 100644
--- a/services/core/jni/com_android_server_display_DisplayControl.cpp
+++ b/services/core/jni/com_android_server_display_DisplayControl.cpp
@@ -24,9 +24,11 @@
 namespace android {
 
 static jobject nativeCreateDisplay(JNIEnv* env, jclass clazz, jstring nameObj, jboolean secure,
-                                   jfloat requestedRefreshRate) {
-    ScopedUtfChars name(env, nameObj);
+                                   jstring uniqueIdStr, jfloat requestedRefreshRate) {
+    const ScopedUtfChars name(env, nameObj);
+    const ScopedUtfChars uniqueId(env, uniqueIdStr);
     sp<IBinder> token(SurfaceComposerClient::createDisplay(String8(name.c_str()), bool(secure),
+                                                           std::string(uniqueId.c_str()),
                                                            requestedRefreshRate));
     return javaObjectForIBinder(env, token);
 }
@@ -178,7 +180,7 @@
 
 static const JNINativeMethod sDisplayMethods[] = {
         // clang-format off
-    {"nativeCreateDisplay", "(Ljava/lang/String;ZF)Landroid/os/IBinder;",
+    {"nativeCreateDisplay", "(Ljava/lang/String;ZLjava/lang/String;F)Landroid/os/IBinder;",
             (void*)nativeCreateDisplay },
     {"nativeDestroyDisplay", "(Landroid/os/IBinder;)V",
             (void*)nativeDestroyDisplay },
diff --git a/services/core/jni/com_android_server_hint_HintManagerService.cpp b/services/core/jni/com_android_server_hint_HintManagerService.cpp
index be18835..2307ace 100644
--- a/services/core/jni/com_android_server_hint_HintManagerService.cpp
+++ b/services/core/jni/com_android_server_hint_HintManagerService.cpp
@@ -34,11 +34,8 @@
 
 #include "jni.h"
 
-using aidl::android::hardware::power::SessionConfig;
-using aidl::android::hardware::power::SessionHint;
-using aidl::android::hardware::power::SessionMode;
-using aidl::android::hardware::power::SessionTag;
-using aidl::android::hardware::power::WorkDuration;
+namespace hal = aidl::android::hardware::power;
+
 using android::power::PowerHintSessionWrapper;
 
 namespace android {
@@ -95,10 +92,11 @@
 
 static jlong createHintSessionWithConfig(JNIEnv* env, int32_t tgid, int32_t uid,
                                          std::vector<int32_t> threadIds, int64_t durationNanos,
-                                         int32_t sessionTag, SessionConfig& config) {
+                                         int32_t sessionTag, hal::SessionConfig& config) {
     auto result =
             gPowerHalController.createHintSessionWithConfig(tgid, uid, threadIds, durationNanos,
-                                                            static_cast<SessionTag>(sessionTag),
+                                                            static_cast<hal::SessionTag>(
+                                                                    sessionTag),
                                                             &config);
     if (result.isOk()) {
         jlong session_ptr = reinterpret_cast<jlong>(result.value().get());
@@ -140,12 +138,12 @@
 }
 
 static void reportActualWorkDuration(int64_t session_ptr,
-                                     const std::vector<WorkDuration>& actualDurations) {
+                                     const std::vector<hal::WorkDuration>& actualDurations) {
     auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->reportActualWorkDuration(actualDurations);
 }
 
-static void sendHint(int64_t session_ptr, SessionHint hint) {
+static void sendHint(int64_t session_ptr, hal::SessionHint hint) {
     auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->sendHint(hint);
 }
@@ -155,7 +153,7 @@
     appSession->setThreads(threadIds);
 }
 
-static void setMode(int64_t session_ptr, SessionMode mode, bool enabled) {
+static void setMode(int64_t session_ptr, hal::SessionMode mode, bool enabled) {
     auto appSession = reinterpret_cast<PowerHintSessionWrapper*>(session_ptr);
     appSession->setMode(mode, enabled);
 }
@@ -189,7 +187,7 @@
         return 0;
     }
     std::vector<int32_t> threadIds(tidArray.get(), tidArray.get() + tidArray.size());
-    SessionConfig config;
+    hal::SessionConfig config;
     jlong out = createHintSessionWithConfig(env, tgid, uid, std::move(threadIds), durationNanos,
                                             sessionTag, config);
     if (out <= 0) {
@@ -223,7 +221,7 @@
     ScopedLongArrayRO arrayActualDurations(env, actualDurations);
     ScopedLongArrayRO arrayTimeStamps(env, timeStamps);
 
-    std::vector<WorkDuration> actualList(arrayActualDurations.size());
+    std::vector<hal::WorkDuration> actualList(arrayActualDurations.size());
     for (size_t i = 0; i < arrayActualDurations.size(); i++) {
         actualList[i].timeStampNanos = arrayTimeStamps[i];
         actualList[i].durationNanos = arrayActualDurations[i];
@@ -232,7 +230,7 @@
 }
 
 static void nativeSendHint(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jint hint) {
-    sendHint(session_ptr, static_cast<SessionHint>(hint));
+    sendHint(session_ptr, static_cast<hal::SessionHint>(hint));
 }
 
 static void nativeSetThreads(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jintArray tids) {
@@ -244,13 +242,13 @@
 
 static void nativeSetMode(JNIEnv* env, jclass /* clazz */, jlong session_ptr, jint mode,
                           jboolean enabled) {
-    setMode(session_ptr, static_cast<SessionMode>(mode), enabled);
+    setMode(session_ptr, static_cast<hal::SessionMode>(mode), enabled);
 }
 
 static void nativeReportActualWorkDuration2(JNIEnv* env, jclass /* clazz */, jlong session_ptr,
                                             jobjectArray jWorkDurations) {
     int size = env->GetArrayLength(jWorkDurations);
-    std::vector<WorkDuration> workDurations(size);
+    std::vector<hal::WorkDuration> workDurations(size);
     for (int i = 0; i < size; i++) {
         jobject workDuration = env->GetObjectArrayElement(jWorkDurations, i);
         workDurations[i].workPeriodStartTimestampNanos =
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 32cb251..a01c123 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -88,7 +88,6 @@
 
 namespace android {
 
-static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer();
 static const bool ENABLE_INPUT_FILTER_RUST = input_flags::enable_input_filter_rust_impl();
 
 // The exponent used to calculate the pointer speed scaling factor.
@@ -128,7 +127,7 @@
     jmethodID getVirtualKeyQuietTimeMillis;
     jmethodID getExcludedDeviceNames;
     jmethodID getInputPortAssociations;
-    jmethodID getInputUniqueIdAssociations;
+    jmethodID getInputUniqueIdAssociationsByPort;
     jmethodID getInputUniqueIdAssociationsByDescriptor;
     jmethodID getDeviceTypeAssociations;
     jmethodID getKeyboardLayoutAssociations;
@@ -273,22 +272,23 @@
     void setDisplayViewports(JNIEnv* env, jobjectArray viewportObjArray);
 
     base::Result<std::unique_ptr<InputChannel>> createInputChannel(const std::string& name);
-    base::Result<std::unique_ptr<InputChannel>> createInputMonitor(int32_t displayId,
+    base::Result<std::unique_ptr<InputChannel>> createInputMonitor(ui::LogicalDisplayId displayId,
                                                                    const std::string& name,
                                                                    gui::Pid pid);
     status_t removeInputChannel(const sp<IBinder>& connectionToken);
     status_t pilferPointers(const sp<IBinder>& token);
 
-    void displayRemoved(JNIEnv* env, int32_t displayId);
-    void setFocusedApplication(JNIEnv* env, int32_t displayId, jobject applicationHandleObj);
-    void setFocusedDisplay(int32_t displayId);
+    void displayRemoved(JNIEnv* env, ui::LogicalDisplayId displayId);
+    void setFocusedApplication(JNIEnv* env, ui::LogicalDisplayId displayId,
+                               jobject applicationHandleObj);
+    void setFocusedDisplay(ui::LogicalDisplayId displayId);
     void setMinTimeBetweenUserActivityPokes(int64_t intervalMillis);
     void setInputDispatchMode(bool enabled, bool frozen);
     void setSystemUiLightsOut(bool lightsOut);
-    void setPointerDisplayId(int32_t displayId);
+    void setPointerDisplayId(ui::LogicalDisplayId displayId);
     int32_t getMousePointerSpeed();
     void setPointerSpeed(int32_t speed);
-    void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled);
+    void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled);
     void setTouchpadPointerSpeed(int32_t speed);
     void setTouchpadNaturalScrollingEnabled(bool enabled);
     void setTouchpadTapToClickEnabled(bool enabled);
@@ -298,25 +298,22 @@
     void setShowTouches(bool enabled);
     void setInteractive(bool interactive);
     void reloadCalibration();
-    void setPointerIconType(PointerIconStyle iconId);
     void reloadPointerIcons();
     void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
-    void setCustomPointerIcon(const SpriteIcon& icon);
     bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon,
-                        int32_t displayId, DeviceId deviceId, int32_t pointerId,
+                        ui::LogicalDisplayId displayId, DeviceId deviceId, int32_t pointerId,
                         const sp<IBinder>& inputToken);
-    void setPointerIconVisibility(int32_t displayId, bool visible);
+    void setPointerIconVisibility(ui::LogicalDisplayId displayId, bool visible);
     void setMotionClassifierEnabled(bool enabled);
     std::optional<std::string> getBluetoothAddress(int32_t deviceId);
     void setStylusButtonMotionEventsEnabled(bool enabled);
-    FloatPoint getMouseCursorPosition(int32_t displayId);
+    FloatPoint getMouseCursorPosition(ui::LogicalDisplayId displayId);
     void setStylusPointerIconEnabled(bool enabled);
     void setInputMethodConnectionIsActive(bool isActive);
 
     /* --- InputReaderPolicyInterface implementation --- */
 
     void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
-    std::shared_ptr<PointerControllerInterface> obtainPointerController(int32_t deviceId) override;
     void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override;
     std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay(
             const InputDeviceIdentifier& identifier,
@@ -329,7 +326,7 @@
     void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
     bool isInputMethodConnectionActive() override;
     std::optional<DisplayViewport> getPointerViewportForAssociatedDisplay(
-            int32_t associatedDisplayId) override;
+            ui::LogicalDisplayId associatedDisplayId) override;
 
     /* --- InputDispatcherPolicyInterface implementation --- */
 
@@ -352,13 +349,15 @@
     void notifyVibratorState(int32_t deviceId, bool isOn) override;
     bool filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) override;
     void interceptKeyBeforeQueueing(const KeyEvent& keyEvent, uint32_t& policyFlags) override;
-    void interceptMotionBeforeQueueing(int32_t displayId, uint32_t source, int32_t action,
-                                       nsecs_t when, uint32_t& policyFlags) override;
+    void interceptMotionBeforeQueueing(ui::LogicalDisplayId displayId, uint32_t source,
+                                       int32_t action, nsecs_t when,
+                                       uint32_t& policyFlags) override;
     nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>& token, const KeyEvent& keyEvent,
                                           uint32_t policyFlags) override;
     std::optional<KeyEvent> dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent& keyEvent,
                                                  uint32_t policyFlags) override;
-    void pokeUserActivity(nsecs_t eventTime, int32_t eventType, int32_t displayId) override;
+    void pokeUserActivity(nsecs_t eventTime, int32_t eventType,
+                          ui::LogicalDisplayId displayId) override;
     void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) override;
     void setPointerCapture(const PointerCaptureRequest& request) override;
     void notifyDropWindow(const sp<IBinder>& token, float x, float y) override;
@@ -367,20 +366,22 @@
 
     /* --- PointerControllerPolicyInterface implementation --- */
 
-    virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId);
-    virtual void loadPointerResources(PointerResources* outResources, int32_t displayId);
+    virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId);
+    virtual void loadPointerResources(PointerResources* outResources,
+                                      ui::LogicalDisplayId displayId);
     virtual void loadAdditionalMouseResources(
             std::map<PointerIconStyle, SpriteIcon>* outResources,
-            std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t displayId);
+            std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
+            ui::LogicalDisplayId displayId);
     virtual PointerIconStyle getDefaultPointerIconId();
     virtual PointerIconStyle getDefaultStylusIconId();
     virtual PointerIconStyle getCustomPointerIconId();
-    virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position);
 
     /* --- PointerChoreographerPolicyInterface implementation --- */
     std::shared_ptr<PointerControllerInterface> createPointerController(
             PointerControllerInterface::ControllerType type) override;
-    void notifyPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
+    void notifyPointerDisplayIdChanged(ui::LogicalDisplayId displayId,
+                                       const FloatPoint& position) override;
 
     /* --- InputFilterPolicyInterface implementation --- */
     void notifyStickyModifierStateChanged(uint32_t modifierState,
@@ -404,24 +405,17 @@
         int32_t pointerSpeed{0};
 
         // Displays on which its associated mice will have pointer acceleration disabled.
-        std::set<int32_t> displaysWithMousePointerAccelerationDisabled{};
+        std::set<ui::LogicalDisplayId> displaysWithMousePointerAccelerationDisabled{};
 
         // True if pointer gestures are enabled.
         bool pointerGesturesEnabled{true};
 
-        // Show touches feature enable/disable.
-        bool showTouches{false};
-
         // The latest request to enable or disable Pointer Capture.
         PointerCaptureRequest pointerCaptureRequest{};
 
         // Sprite controller singleton, created on first use.
         std::shared_ptr<SpriteController> spriteController{};
 
-        // TODO(b/293587049): Remove when the PointerChoreographer refactoring is complete.
-        // Pointer controller singleton, created and destroyed as needed.
-        std::weak_ptr<PointerController> legacyPointerController{};
-
         // The list of PointerControllers created and managed by the PointerChoreographer.
         std::list<std::weak_ptr<PointerController>> pointerControllers{};
 
@@ -429,7 +423,7 @@
         std::set<int32_t> disabledInputDevices{};
 
         // Associated Pointer controller display.
-        int32_t pointerDisplayId{ADISPLAY_ID_DEFAULT};
+        ui::LogicalDisplayId pointerDisplayId{ui::ADISPLAY_ID_DEFAULT};
 
         // True if stylus button reporting through motion events is enabled.
         bool stylusButtonMotionEventsEnabled{true};
@@ -462,7 +456,7 @@
     void updateInactivityTimeoutLocked();
     void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
     void ensureSpriteControllerLocked();
-    sp<SurfaceControl> getParentSurfaceForPointers(int displayId);
+    sp<SurfaceControl> getParentSurfaceForPointers(ui::LogicalDisplayId displayId);
     static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName);
     template <typename T>
     std::unordered_map<std::string, T> readMapFromInterleavedJavaArray(
@@ -471,7 +465,7 @@
 
     void forEachPointerControllerLocked(std::function<void(PointerController&)> apply)
             REQUIRES(mLock);
-    PointerIcon loadPointerIcon(JNIEnv* env, int32_t displayId, PointerIconStyle type);
+    PointerIcon loadPointerIcon(JNIEnv* env, ui::LogicalDisplayId displayId, PointerIconStyle type);
 
     static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); }
 };
@@ -502,16 +496,14 @@
                              toString(mLocked.systemUiLightsOut));
         dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
         dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n",
-                             dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str());
+                             dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled,
+                                     streamableToString)
+                                     .c_str());
         dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n",
-                toString(mLocked.pointerGesturesEnabled));
-        dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches));
+                             toString(mLocked.pointerGesturesEnabled));
         dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n",
                              mLocked.pointerCaptureRequest.isEnable() ? "Enabled" : "Disabled",
                              mLocked.pointerCaptureRequest.seq);
-        if (auto pc = mLocked.legacyPointerController.lock(); pc) {
-            dump += pc->dump();
-        }
     } // release lock
     dump += "\n";
 
@@ -556,9 +548,7 @@
                 [&viewports](PointerController& pc) { pc.onDisplayViewportsUpdated(viewports); });
     } // release lock
 
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        mInputManager->getChoreographer().setDisplayViewports(viewports);
-    }
+    mInputManager->getChoreographer().setDisplayViewports(viewports);
     mInputManager->getReader().requestRefreshConfiguration(
             InputReaderConfiguration::Change::DISPLAY_INFO);
 }
@@ -570,7 +560,7 @@
 }
 
 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputMonitor(
-        int32_t displayId, const std::string& name, gui::Pid pid) {
+        ui::LogicalDisplayId displayId, const std::string& name, gui::Pid pid) {
     ATRACE_CALL();
     return mInputManager->getDispatcher().createInputMonitor(displayId, name, pid);
 }
@@ -634,10 +624,9 @@
         env->DeleteLocalRef(portAssociations);
     }
 
-    outConfig->uniqueIdAssociationsByPort =
-            readMapFromInterleavedJavaArray<std::string>(gServiceClassInfo
-                                                                 .getInputUniqueIdAssociations,
-                                                         "getInputUniqueIdAssociations");
+    outConfig->uniqueIdAssociationsByPort = readMapFromInterleavedJavaArray<
+            std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByPort,
+                         "getInputUniqueIdAssociationsByPort");
 
     outConfig->uniqueIdAssociationsByDescriptor = readMapFromInterleavedJavaArray<
             std::string>(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor,
@@ -700,8 +689,6 @@
                 : 1;
         outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;
 
-        outConfig->showTouches = mLocked.showTouches;
-
         outConfig->pointerCaptureRequest = mLocked.pointerCaptureRequest;
 
         outConfig->setDisplayViewports(mLocked.viewports);
@@ -743,10 +730,6 @@
 
 void NativeInputManager::forEachPointerControllerLocked(
         std::function<void(PointerController&)> apply) {
-    if (auto pc = mLocked.legacyPointerController.lock(); pc) {
-        apply(*pc);
-    }
-
     auto it = mLocked.pointerControllers.begin();
     while (it != mLocked.pointerControllers.end()) {
         auto pc = it->lock();
@@ -759,7 +742,7 @@
     }
 }
 
-PointerIcon NativeInputManager::loadPointerIcon(JNIEnv* env, int32_t displayId,
+PointerIcon NativeInputManager::loadPointerIcon(JNIEnv* env, ui::LogicalDisplayId displayId,
                                                 PointerIconStyle type) {
     if (type == PointerIconStyle::TYPE_CUSTOM) {
         LOG(FATAL) << __func__ << ": Cannot load non-system icon type";
@@ -780,51 +763,17 @@
     return android_view_PointerIcon_toNative(env, pointerIconObj.get());
 }
 
-// TODO(b/293587049): Remove the old way of obtaining PointerController when the
-//  PointerChoreographer refactoring is complete.
-std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController(
-        int32_t /* deviceId */) {
-    ATRACE_CALL();
-    std::scoped_lock _l(mLock);
-
-    std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock();
-    if (controller == nullptr) {
-        ensureSpriteControllerLocked();
-
-        // Disable the functionality of the legacy PointerController if PointerChoreographer is
-        // enabled.
-        controller = PointerController::create(this, mLooper, *mLocked.spriteController,
-                                               /*enabled=*/!ENABLE_POINTER_CHOREOGRAPHER);
-        mLocked.legacyPointerController = controller;
-        updateInactivityTimeoutLocked();
-    }
-
-    return controller;
-}
-
 std::shared_ptr<PointerControllerInterface> NativeInputManager::createPointerController(
         PointerControllerInterface::ControllerType type) {
     std::scoped_lock _l(mLock);
     ensureSpriteControllerLocked();
     std::shared_ptr<PointerController> pc =
-            PointerController::create(this, mLooper, *mLocked.spriteController, /*enabled=*/true,
-                                      type);
+            PointerController::create(this, mLooper, *mLocked.spriteController, type);
     mLocked.pointerControllers.emplace_back(pc);
     return pc;
 }
 
-void NativeInputManager::onPointerDisplayIdChanged(int32_t pointerDisplayId,
-                                                   const FloatPoint& position) {
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        return;
-    }
-    JNIEnv* env = jniEnv();
-    env->CallVoidMethod(mServiceObj, gServiceClassInfo.onPointerDisplayIdChanged, pointerDisplayId,
-                        position.x, position.y);
-    checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged");
-}
-
-void NativeInputManager::notifyPointerDisplayIdChanged(int32_t pointerDisplayId,
+void NativeInputManager::notifyPointerDisplayIdChanged(ui::LogicalDisplayId pointerDisplayId,
                                                        const FloatPoint& position) {
     // Notify the Reader so that devices can be reconfigured.
     { // acquire lock
@@ -833,7 +782,7 @@
             return;
         }
         mLocked.pointerDisplayId = pointerDisplayId;
-        ALOGI("%s: pointer displayId set to: %d", __func__, pointerDisplayId);
+        ALOGI("%s: pointer displayId set to: %s", __func__, pointerDisplayId.toString().c_str());
     } // release lock
     mInputManager->getReader().requestRefreshConfiguration(
             InputReaderConfiguration::Change::DISPLAY_INFO);
@@ -853,7 +802,7 @@
     checkAndClearExceptionFromCallback(env, "notifyStickyModifierStateChanged");
 }
 
-sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(int displayId) {
+sp<SurfaceControl> NativeInputManager::getParentSurfaceForPointers(ui::LogicalDisplayId displayId) {
     JNIEnv* env = jniEnv();
     jlong nativeSurfaceControlPtr =
             env->CallLongMethod(mServiceObj, gServiceClassInfo.getParentSurfaceForPointers,
@@ -875,9 +824,10 @@
         layer = -1;
     }
     mLocked.spriteController =
-            std::make_shared<SpriteController>(mLooper, layer, [this](int displayId) {
-                return getParentSurfaceForPointers(displayId);
-            });
+            std::make_shared<SpriteController>(mLooper, layer,
+                                               [this](ui::LogicalDisplayId displayId) {
+                                                   return getParentSurfaceForPointers(displayId);
+                                               });
     // The SpriteController needs to be shared pointer because the handler callback needs to hold
     // a weak reference so that we can avoid racy conditions when the controller is being destroyed.
     mLocked.spriteController->setHandlerController(mLocked.spriteController);
@@ -1079,8 +1029,7 @@
 
     jobject tokenObj = javaObjectForIBinder(env, token);
     if (tokenObj) {
-        env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyInputChannelBroken,
-                tokenObj);
+        env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyInputChannelBroken, tokenObj);
         checkAndClearExceptionFromCallback(env, "notifyInputChannelBroken");
     }
 }
@@ -1166,12 +1115,12 @@
     checkAndClearExceptionFromCallback(env, "notifyVibratorState");
 }
 
-void NativeInputManager::displayRemoved(JNIEnv* env, int32_t displayId) {
+void NativeInputManager::displayRemoved(JNIEnv* env, ui::LogicalDisplayId displayId) {
     mInputManager->getDispatcher().displayRemoved(displayId);
 }
 
-void NativeInputManager::setFocusedApplication(JNIEnv* env, int32_t displayId,
-        jobject applicationHandleObj) {
+void NativeInputManager::setFocusedApplication(JNIEnv* env, ui::LogicalDisplayId displayId,
+                                               jobject applicationHandleObj) {
     if (!applicationHandleObj) {
         return;
     }
@@ -1181,7 +1130,7 @@
     mInputManager->getDispatcher().setFocusedApplication(displayId, applicationHandle);
 }
 
-void NativeInputManager::setFocusedDisplay(int32_t displayId) {
+void NativeInputManager::setFocusedDisplay(ui::LogicalDisplayId displayId) {
     mInputManager->getDispatcher().setFocusedDisplay(displayId);
 }
 
@@ -1209,24 +1158,8 @@
     });
 }
 
-void NativeInputManager::setPointerDisplayId(int32_t displayId) {
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId);
-    } else {
-        { // acquire lock
-            std::scoped_lock _l(mLock);
-
-            if (mLocked.pointerDisplayId == displayId) {
-                return;
-            }
-
-            ALOGI("Setting pointer display id to %d.", displayId);
-            mLocked.pointerDisplayId = displayId;
-        } // release lock
-
-        mInputManager->getReader().requestRefreshConfiguration(
-                InputReaderConfiguration::Change::DISPLAY_INFO);
-    }
+void NativeInputManager::setPointerDisplayId(ui::LogicalDisplayId displayId) {
+    mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId);
 }
 
 int32_t NativeInputManager::getMousePointerSpeed() {
@@ -1250,7 +1183,8 @@
             InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
-void NativeInputManager::setMousePointerAccelerationEnabled(int32_t displayId, bool enabled) {
+void NativeInputManager::setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId,
+                                                            bool enabled) {
     { // acquire lock
         std::scoped_lock _l(mLock);
 
@@ -1260,8 +1194,8 @@
             return;
         }
 
-        ALOGI("Setting mouse pointer acceleration to %s on display %d", toString(enabled),
-              displayId);
+        ALOGI("Setting mouse pointer acceleration to %s on display %s", toString(enabled),
+              displayId.toString().c_str());
         if (enabled) {
             mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId);
         } else {
@@ -1378,24 +1312,7 @@
 }
 
 void NativeInputManager::setShowTouches(bool enabled) {
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        mInputManager->getChoreographer().setShowTouchesEnabled(enabled);
-        return;
-    }
-
-    { // acquire lock
-        std::scoped_lock _l(mLock);
-
-        if (mLocked.showTouches == enabled) {
-            return;
-        }
-
-        ALOGI("Setting show touches feature to %s.", enabled ? "enabled" : "disabled");
-        mLocked.showTouches = enabled;
-    } // release lock
-
-    mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::Change::SHOW_TOUCHES);
+    mInputManager->getChoreographer().setShowTouchesEnabled(enabled);
 }
 
 void NativeInputManager::requestPointerCapture(const sp<IBinder>& windowToken, bool enabled) {
@@ -1411,30 +1328,15 @@
             InputReaderConfiguration::Change::TOUCH_AFFINE_TRANSFORMATION);
 }
 
-void NativeInputManager::setPointerIconType(PointerIconStyle iconId) {
-    std::scoped_lock _l(mLock);
-    std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock();
-    if (controller != nullptr) {
-        controller->updatePointerIcon(iconId);
-    }
-}
-
 void NativeInputManager::reloadPointerIcons() {
     std::scoped_lock _l(mLock);
     forEachPointerControllerLocked([](PointerController& pc) { pc.reloadPointerResources(); });
 }
 
-void NativeInputManager::setCustomPointerIcon(const SpriteIcon& icon) {
-    std::scoped_lock _l(mLock);
-    std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock();
-    if (controller != nullptr) {
-        controller->setCustomPointerIcon(icon);
-    }
-}
-
 bool NativeInputManager::setPointerIcon(
-        std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon, int32_t displayId,
-        DeviceId deviceId, int32_t pointerId, const sp<IBinder>& inputToken) {
+        std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon,
+        ui::LogicalDisplayId displayId, DeviceId deviceId, int32_t pointerId,
+        const sp<IBinder>& inputToken) {
     if (!mInputManager->getDispatcher().isPointerInWindow(inputToken, displayId, deviceId,
                                                           pointerId)) {
         LOG(WARNING) << "Attempted to change the pointer icon for deviceId " << deviceId
@@ -1446,10 +1348,7 @@
     return mInputManager->getChoreographer().setPointerIcon(std::move(icon), displayId, deviceId);
 }
 
-void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) {
-    if (!ENABLE_POINTER_CHOREOGRAPHER) {
-        return;
-    }
+void NativeInputManager::setPointerIconVisibility(ui::LogicalDisplayId displayId, bool visible) {
     mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible);
 }
 
@@ -1504,7 +1403,7 @@
 }
 
 std::optional<DisplayViewport> NativeInputManager::getPointerViewportForAssociatedDisplay(
-        int32_t associatedDisplayId) {
+        ui::LogicalDisplayId associatedDisplayId) {
     return mInputManager->getChoreographer().getViewportForPointerDevice(associatedDisplayId);
 }
 
@@ -1579,9 +1478,9 @@
     handleInterceptActions(wmActions, when, /*byref*/ policyFlags);
 }
 
-void NativeInputManager::interceptMotionBeforeQueueing(int32_t displayId, uint32_t source,
-                                                       int32_t action, nsecs_t when,
-                                                       uint32_t& policyFlags) {
+void NativeInputManager::interceptMotionBeforeQueueing(ui::LogicalDisplayId displayId,
+                                                       uint32_t source, int32_t action,
+                                                       nsecs_t when, uint32_t& policyFlags) {
     ATRACE_CALL();
     // Policy:
     // - Ignore untrusted events and pass them along.
@@ -1700,7 +1599,8 @@
     return fallbackEvent;
 }
 
-void NativeInputManager::pokeUserActivity(nsecs_t eventTime, int32_t eventType, int32_t displayId) {
+void NativeInputManager::pokeUserActivity(nsecs_t eventTime, int32_t eventType,
+                                          ui::LogicalDisplayId displayId) {
     ATRACE_CALL();
     android_server_PowerManagerService_userActivity(eventTime, eventType, displayId);
 }
@@ -1731,13 +1631,14 @@
             InputReaderConfiguration::Change::POINTER_CAPTURE);
 }
 
-void NativeInputManager::loadPointerIcon(SpriteIcon* icon, int32_t displayId) {
+void NativeInputManager::loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) {
     ATRACE_CALL();
     JNIEnv* env = jniEnv();
     *icon = toSpriteIcon(loadPointerIcon(env, displayId, PointerIconStyle::TYPE_ARROW));
 }
 
-void NativeInputManager::loadPointerResources(PointerResources* outResources, int32_t displayId) {
+void NativeInputManager::loadPointerResources(PointerResources* outResources,
+                                              ui::LogicalDisplayId displayId) {
     ATRACE_CALL();
     JNIEnv* env = jniEnv();
 
@@ -1751,7 +1652,8 @@
 
 void NativeInputManager::loadAdditionalMouseResources(
         std::map<PointerIconStyle, SpriteIcon>* outResources,
-        std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t displayId) {
+        std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
+        ui::LogicalDisplayId displayId) {
     ATRACE_CALL();
     JNIEnv* env = jniEnv();
 
@@ -1818,37 +1720,13 @@
             InputReaderConfiguration::Change::STYLUS_BUTTON_REPORTING);
 }
 
-FloatPoint NativeInputManager::getMouseCursorPosition(int32_t displayId) {
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        return mInputManager->getChoreographer().getMouseCursorPosition(displayId);
-    }
-    // To maintain the status-quo, the displayId parameter (used when PointerChoreographer is
-    // enabled) is ignored in the old pipeline.
-    std::scoped_lock _l(mLock);
-    const auto pc = mLocked.legacyPointerController.lock();
-    if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION};
-
-    return pc->getPosition();
+FloatPoint NativeInputManager::getMouseCursorPosition(ui::LogicalDisplayId displayId) {
+    return mInputManager->getChoreographer().getMouseCursorPosition(displayId);
 }
 
 void NativeInputManager::setStylusPointerIconEnabled(bool enabled) {
-    if (ENABLE_POINTER_CHOREOGRAPHER) {
-        mInputManager->getChoreographer().setStylusPointerIconEnabled(enabled);
-        return;
-    }
-
-    { // acquire lock
-        std::scoped_lock _l(mLock);
-
-        if (mLocked.stylusPointerIconEnabled == enabled) {
-            return;
-        }
-
-        mLocked.stylusPointerIconEnabled = enabled;
-    } // release lock
-
-    mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::Change::DISPLAY_INFO);
+    mInputManager->getChoreographer().setStylusPointerIconEnabled(enabled);
+    return;
 }
 
 void NativeInputManager::setInputMethodConnectionIsActive(bool isActive) {
@@ -2008,7 +1886,7 @@
                                         jstring nameObj, jint pid) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    if (displayId == ADISPLAY_ID_NONE) {
+    if (displayId == ui::ADISPLAY_ID_NONE.val()) {
         std::string message = "InputChannel used as a monitor must be associated with a display";
         jniThrowRuntimeException(env, message.c_str());
         return nullptr;
@@ -2018,7 +1896,7 @@
     std::string name = nameChars.c_str();
 
     base::Result<std::unique_ptr<InputChannel>> inputChannel =
-            im->createInputMonitor(displayId, name, gui::Pid{pid});
+            im->createInputMonitor(ui::LogicalDisplayId{displayId}, name, gui::Pid{pid});
 
     if (!inputChannel.ok()) {
         std::string message = inputChannel.error().message();
@@ -2065,7 +1943,8 @@
 
     return im->getInputManager()->getDispatcher().setInTouchMode(inTouchMode, gui::Pid{pid},
                                                                  gui::Uid{static_cast<uid_t>(uid)},
-                                                                 hasPermission, displayId);
+                                                                 hasPermission,
+                                                                 ui::LogicalDisplayId{displayId});
 }
 
 static void nativeSetMaximumObscuringOpacityForTouch(JNIEnv* env, jobject nativeImplObj,
@@ -2157,20 +2036,20 @@
 static void nativeDisplayRemoved(JNIEnv* env, jobject nativeImplObj, jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->displayRemoved(env, displayId);
+    im->displayRemoved(env, ui::LogicalDisplayId{displayId});
 }
 
 static void nativeSetFocusedApplication(JNIEnv* env, jobject nativeImplObj, jint displayId,
                                         jobject applicationHandleObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setFocusedApplication(env, displayId, applicationHandleObj);
+    im->setFocusedApplication(env, ui::LogicalDisplayId{displayId}, applicationHandleObj);
 }
 
 static void nativeSetFocusedDisplay(JNIEnv* env, jobject nativeImplObj, jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setFocusedDisplay(displayId);
+    im->setFocusedDisplay(ui::LogicalDisplayId{displayId});
 }
 
 static void nativeSetUserActivityPokeInterval(JNIEnv* env, jobject nativeImplObj,
@@ -2226,8 +2105,8 @@
 
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     if (im->getInputManager()->getDispatcher().transferTouchOnDisplay(destChannelToken,
-                                                                      static_cast<int32_t>(
-                                                                              displayId))) {
+                                                                      ui::LogicalDisplayId{
+                                                                              displayId})) {
         return JNI_TRUE;
     } else {
         return JNI_FALSE;
@@ -2250,7 +2129,7 @@
                                                      jint displayId, jboolean enabled) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setMousePointerAccelerationEnabled(displayId, enabled);
+    im->setMousePointerAccelerationEnabled(ui::LogicalDisplayId{displayId}, enabled);
 }
 
 static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
@@ -2597,27 +2476,12 @@
     im->setInputDeviceEnabled(deviceId, false);
 }
 
-static void nativeSetPointerIconType(JNIEnv* env, jobject nativeImplObj, jint iconId) {
-    // iconId is set in java from from frameworks/base/core/java/android/view/PointerIcon.java,
-    // where the definition in <input/Input.h> is duplicated as a sealed class (type safe enum
-    // equivalent in Java).
-
-    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-
-    im->setPointerIconType(static_cast<PointerIconStyle>(iconId));
-}
-
 static void nativeReloadPointerIcons(JNIEnv* env, jobject nativeImplObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->reloadPointerIcons();
 }
 
-static void nativeSetCustomPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj) {
-    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    im->setCustomPointerIcon(toSpriteIcon(android_view_PointerIcon_toNative(env, iconObj)));
-}
-
 static bool nativeSetPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj,
                                  jint displayId, jint deviceId, jint pointerId,
                                  jobject inputTokenObj) {
@@ -2635,7 +2499,7 @@
         icon = pointerIcon.style;
     }
 
-    return im->setPointerIcon(std::move(icon), displayId, deviceId, pointerId,
+    return im->setPointerIcon(std::move(icon), ui::LogicalDisplayId{displayId}, deviceId, pointerId,
                               ibinderForJavaObject(env, inputTokenObj));
 }
 
@@ -2643,13 +2507,14 @@
                                            jboolean visible) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setPointerIconVisibility(displayId, visible);
+    im->setPointerIconVisibility(ui::LogicalDisplayId{displayId}, visible);
 }
 
 static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                            jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    return im->getInputManager()->getReader().canDispatchToDisplay(deviceId, displayId);
+    return im->getInputManager()->getReader().canDispatchToDisplay(deviceId,
+                                                                   ui::LogicalDisplayId{displayId});
 }
 
 static void nativeNotifyPortAssociationsChanged(JNIEnv* env, jobject nativeImplObj) {
@@ -2661,8 +2526,9 @@
 static void nativeSetDisplayEligibilityForPointerCapture(JNIEnv* env, jobject nativeImplObj,
                                                          jint displayId, jboolean isEligible) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    im->getInputManager()->getDispatcher().setDisplayEligibilityForPointerCapture(displayId,
-                                                                                  isEligible);
+    im->getInputManager()
+            ->getDispatcher()
+            .setDisplayEligibilityForPointerCapture(ui::LogicalDisplayId{displayId}, isEligible);
 }
 
 static void nativeChangeUniqueIdAssociation(JNIEnv* env, jobject nativeImplObj) {
@@ -2798,7 +2664,7 @@
 
 static void nativeSetPointerDisplayId(JNIEnv* env, jobject nativeImplObj, jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    im->setPointerDisplayId(displayId);
+    im->setPointerDisplayId(ui::LogicalDisplayId{displayId});
 }
 
 static jstring nativeGetBluetoothAddress(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
@@ -2816,7 +2682,7 @@
 static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplObj,
                                                 jint displayId) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
-    const auto p = im->getMouseCursorPosition(displayId);
+    const auto p = im->getMouseCursorPosition(ui::LogicalDisplayId{displayId});
     const std::array<float, 2> arr = {{p.x, p.y}};
     jfloatArray outArr = env->NewFloatArray(2);
     env->SetFloatArrayRegion(outArr, 0, arr.size(), arr.data());
@@ -2937,10 +2803,7 @@
         {"isInputDeviceEnabled", "(I)Z", (void*)nativeIsInputDeviceEnabled},
         {"enableInputDevice", "(I)V", (void*)nativeEnableInputDevice},
         {"disableInputDevice", "(I)V", (void*)nativeDisableInputDevice},
-        {"setPointerIconType", "(I)V", (void*)nativeSetPointerIconType},
         {"reloadPointerIcons", "()V", (void*)nativeReloadPointerIcons},
-        {"setCustomPointerIcon", "(Landroid/view/PointerIcon;)V",
-         (void*)nativeSetCustomPointerIcon},
         {"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z",
          (void*)nativeSetPointerIcon},
         {"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility},
@@ -3082,8 +2945,8 @@
     GET_METHOD_ID(gServiceClassInfo.getInputPortAssociations, clazz,
             "getInputPortAssociations", "()[Ljava/lang/String;");
 
-    GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociations, clazz,
-                  "getInputUniqueIdAssociations", "()[Ljava/lang/String;");
+    GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociationsByPort, clazz,
+                  "getInputUniqueIdAssociationsByPort", "()[Ljava/lang/String;");
 
     GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociationsByDescriptor, clazz,
                   "getInputUniqueIdAssociationsByDescriptor", "()[Ljava/lang/String;");
diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp
index d0b290c..0733968 100644
--- a/services/core/jni/com_android_server_power_PowerManagerService.cpp
+++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp
@@ -99,7 +99,7 @@
 }
 
 void android_server_PowerManagerService_userActivity(nsecs_t eventTime, int32_t eventType,
-                                                     int32_t displayId) {
+                                                     ui::LogicalDisplayId displayId) {
     if (gPowerManagerServiceObj) {
         // Throttle calls into user activity by event type.
         // We're a little conservative about argument checking here in case the caller
@@ -124,8 +124,8 @@
         JNIEnv* env = AndroidRuntime::getJNIEnv();
 
         env->CallVoidMethod(gPowerManagerServiceObj,
-                gPowerManagerServiceClassInfo.userActivityFromNative,
-                nanoseconds_to_milliseconds(eventTime), eventType, displayId, 0);
+                            gPowerManagerServiceClassInfo.userActivityFromNative,
+                            nanoseconds_to_milliseconds(eventTime), eventType, displayId.val(), 0);
         checkAndClearExceptionFromCallback(env, "userActivityFromNative");
     }
 }
diff --git a/services/core/jni/com_android_server_power_PowerManagerService.h b/services/core/jni/com_android_server_power_PowerManagerService.h
index 36aaceb..ed7fa7c 100644
--- a/services/core/jni/com_android_server_power_PowerManagerService.h
+++ b/services/core/jni/com_android_server_power_PowerManagerService.h
@@ -19,6 +19,7 @@
 
 #include <nativehelper/JNIHelp.h>
 #include <powermanager/PowerManager.h>
+#include <ui/LogicalDisplayId.h>
 #include <utils/Timers.h>
 
 #include "jni.h"
@@ -26,7 +27,7 @@
 namespace android {
 
 extern void android_server_PowerManagerService_userActivity(nsecs_t eventTime, int32_t eventType,
-                                                            int32_t displayId);
+                                                            ui::LogicalDisplayId displayId);
 
 } // namespace android
 
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index 0c83e8e..2e126f1 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -295,7 +295,7 @@
         }
     }
 
-    private static Set<ComponentName> getPrimaryProvidersForUserId(Context context, int userId) {
+    static Set<ComponentName> getPrimaryProvidersForUserId(Context context, int userId) {
         final int resolvedUserId = ActivityManager.handleIncomingUser(
                 Binder.getCallingPid(), Binder.getCallingUid(),
                 userId, false, false,
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index 38ad5b6..b86db06 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.credentials;
 
+import static com.android.server.credentials.CredentialManagerService.getPrimaryProvidersForUserId;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -30,6 +32,7 @@
 import com.android.server.infra.AbstractPerUserSystemService;
 
 import java.util.List;
+import java.util.Set;
 
 
 /**
@@ -80,9 +83,12 @@
             Slog.i(TAG, "newServiceInfoLocked, mInfo null, "
                     + serviceComponent.flattenToString());
         }
+        Set<ComponentName> primaryProviders =
+                getPrimaryProvidersForUserId(mMaster.getContext(), mUserId);
         mInfo = CredentialProviderInfoFactory.create(
                 getContext(), serviceComponent,
-                mUserId, /*isSystemProvider=*/false);
+                mUserId, /*isSystemProvider=*/false,
+                primaryProviders.contains(serviceComponent));
         return mInfo.getServiceInfo();
     }
 
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index b1673e2..7a026d5 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -67,6 +67,9 @@
 
     private final ResultReceiver mAutofillCallback;
 
+    @Nullable
+    private ComponentName mPrimaryProviderComponentName = null;
+
     public GetCandidateRequestSession(
             Context context, SessionLifetime sessionCallback,
             Object lock, int userId, int callingUid,
@@ -104,8 +107,12 @@
         if (providerGetCandidateSessions != null) {
             Slog.d(TAG, "In startProviderSession - provider session created and "
                     + "being added for: " + providerInfo.getComponentName());
-            mProviders.put(providerGetCandidateSessions.getComponentName().flattenToString(),
-                    providerGetCandidateSessions);
+            ComponentName componentName = providerGetCandidateSessions
+                    .getComponentName();
+            if (providerInfo.isPrimary()) {
+                mPrimaryProviderComponentName = componentName;
+            }
+            mProviders.put(componentName.flattenToString(), providerGetCandidateSessions);
         }
         return providerGetCandidateSessions;
     }
@@ -138,7 +145,7 @@
 
         try {
             invokeClientCallbackSuccess(new GetCandidateCredentialsResponse(
-                    candidateProviderDataList, intent));
+                    candidateProviderDataList, intent, mPrimaryProviderComponentName));
         } catch (RemoteException e) {
             Slog.e(TAG, "Issue while responding to client with error : " + e);
         }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 375fc5a..9eb7b22 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -21605,12 +21605,9 @@
                                 == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
             }
 
-            if (Flags.headlessSingleMinTargetSdk()
-                    && mInjector.userManagerIsHeadlessSystemUserMode()
-                    && isSingleUserMode
-                    && !mInjector.isChangeEnabled(
-                            PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(),
-                    caller.getUserId())) {
+            if (Flags.headlessSingleUserFixes() && mInjector.userManagerIsHeadlessSystemUserMode()
+                    && isSingleUserMode && !mInjector.isChangeEnabled(
+                    PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) {
                 throw new IllegalStateException("Device admin is not targeting Android V.");
             }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index 4bf3ff4..09eef45 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -196,19 +196,27 @@
         Binder.withCleanCallingIdentity(() -> {
             PackageManagerInternal pmi =
                     LocalServices.getService(PackageManagerInternal.class);
+            AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+
             pmi.setOwnerProtectedPackages(userId,
                     packages == null ? null : packages.stream().toList());
             LocalServices.getService(UsageStatsManagerInternal.class)
                     .setAdminProtectedPackages(
                             packages == null ? null : new ArraySet<>(packages), userId);
 
-            if (Flags.disallowUserControlBgUsageFix()) {
-                if (packages == null) {
-                    return;
+            if (packages == null || packages.isEmpty()) {
+                return;
+            }
+
+            for (int user : resolveUsers(userId)) {
+                if (Flags.disallowUserControlBgUsageFix()) {
+                    setBgUsageAppOp(packages, pmi, user, appOpsManager);
                 }
-                final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
-                resolveUsers(userId).forEach(
-                        user -> setBgUsageAppOp(packages, pmi, user, appOpsManager));
+                if (Flags.disallowUserControlStoppedStateFix()) {
+                    for (String packageName : packages) {
+                        pmi.setPackageStoppedState(packageName, false, user);
+                    }
+                }
             }
         });
         return true;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8755a80..cfe4e17 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -47,6 +47,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.content.res.Resources.Theme;
+import android.crashrecovery.flags.Flags;
 import android.credentials.CredentialManager;
 import android.database.sqlite.SQLiteCompatibilityWalFlags;
 import android.database.sqlite.SQLiteGlobal;
@@ -1195,11 +1196,13 @@
         mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class);
         t.traceEnd();
 
-        // Now that we have the bare essentials of the OS up and running, take
-        // note that we just booted, which might send out a rescue party if
-        // we're stuck in a runtime restart loop.
-        RescueParty.registerHealthObserver(mSystemContext);
-        PackageWatchdog.getInstance(mSystemContext).noteBoot();
+        if (!Flags.recoverabilityDetection()) {
+            // Now that we have the bare essentials of the OS up and running, take
+            // note that we just booted, which might send out a rescue party if
+            // we're stuck in a runtime restart loop.
+            RescueParty.registerHealthObserver(mSystemContext);
+            PackageWatchdog.getInstance(mSystemContext).noteBoot();
+        }
 
         // Manages LEDs and display backlight so we need it to bring up the display.
         t.traceBegin("StartLightsService");
@@ -1469,9 +1472,12 @@
         boolean enableVrService = context.getPackageManager().hasSystemFeature(
                 PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
 
-        // For debugging RescueParty
-        if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.crash_system", false)) {
-            throw new RuntimeException();
+        if (!Flags.recoverabilityDetection()) {
+            // For debugging RescueParty
+            if (Build.IS_DEBUGGABLE
+                    && SystemProperties.getBoolean("debug.crash_system", false)) {
+                throw new RuntimeException();
+            }
         }
 
         try {
@@ -2910,6 +2916,14 @@
         mPackageManagerService.systemReady();
         t.traceEnd();
 
+        if (Flags.recoverabilityDetection()) {
+            // Now that we have the essential services needed for rescue party, initialize
+            // RescuParty. note that we just booted, which might send out a rescue party if
+            // we're stuck in a runtime restart loop.
+            RescueParty.registerHealthObserver(mSystemContext);
+            PackageWatchdog.getInstance(mSystemContext).noteBoot();
+        }
+
         t.traceBegin("MakeDisplayManagerServiceReady");
         try {
             // TODO: use boot phase and communicate this flag some other way
@@ -3313,6 +3327,14 @@
      * are updated outside of OTA; and to avoid breaking dependencies from system into apexes.
      */
     private void startApexServices(@NonNull TimingsTraceAndSlog t) {
+        if (Flags.recoverabilityDetection()) {
+            // For debugging RescueParty
+            if (Build.IS_DEBUGGABLE
+                    && SystemProperties.getBoolean("debug.crash_system", false)) {
+                throw new RuntimeException();
+            }
+        }
+
         t.traceBegin("startApexServices");
         // TODO(b/192880996): get the list from "android" package, once the manifest entries
         // are migrated to system 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 b155829..6499556 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
@@ -2046,8 +2046,20 @@
                     writer.println("Unknown app ID $appId.")
                 }
             }
+        } else if (args[0] == "--package" && args.size == 2) {
+            val packageName = args[1]
+            service.getState {
+                val packageState = state.externalState.packageStates[packageName]
+                if (packageState != null) {
+                    writer.dumpAppIdState(packageState.appId, state, indexedSetOf(packageName))
+                } else {
+                    writer.println("Unknown package $packageName.")
+                }
+            }
         } else {
-            writer.println("Usage: dumpsys permission [--app-id APP_ID]")
+            writer.println(
+                "Usage: dumpsys permissionmgr [--app-id <APP_ID>] [--package <PACKAGE_NAME>]"
+            )
         }
     }
 
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index 622e702..54d101a 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -243,12 +243,12 @@
                                 return;
                             }
                             sSelfService.mIProfcollect.process();
-                            jobFinished(params, false);
                         } catch (RemoteException e) {
                             Log.e(LOG_TAG, "Failed to process profiles in background: "
                                     + e.getMessage());
                         }
                     });
+            jobFinished(params, false);
             return true;
         }
 
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 1666fef..54b2d4d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -282,7 +282,7 @@
             return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
                     new VirtualDisplayAdapter.SurfaceControlDisplayFactory() {
                         @Override
-                        public IBinder createDisplay(String name, boolean secure,
+                        public IBinder createDisplay(String name, boolean secure, String uniqueId,
                                 float requestedRefreshRate) {
                             return mMockDisplayToken;
                         }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 80f38eb..e5685c7 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1790,6 +1790,7 @@
         verify(mHolder.animator).animateTo(eq(brightness),
                 /* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(),
                 /* ignoreAnimationLimits= */ anyBoolean());
+        verify(mHolder.brightnessSetting).setBrightness(brightness);
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index 4a21645..42814e7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -239,6 +239,9 @@
 
     @Test
     public void testBootLoopDetectionWithExecutionForAllRescueLevels() {
+        // this is old test where the flag needs to be disabled
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
         RescueParty.onSettingsProviderPublished(mMockContext);
         verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
                 any(Executor.class),
@@ -449,6 +452,9 @@
 
     @Test
     public void testNonPersistentAppCrashDetectionWithScopedResets() {
+        // this is old test where the flag needs to be disabled
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
         RescueParty.onSettingsProviderPublished(mMockContext);
         verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
                 any(Executor.class),
@@ -506,6 +512,9 @@
 
     @Test
     public void testNonDeviceConfigSettingsOnlyResetOncePerLevel() {
+        // this is old test where the flag needs to be disabled
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
         RescueParty.onSettingsProviderPublished(mMockContext);
         verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
                 any(Executor.class),
@@ -879,6 +888,9 @@
 
     @Test
     public void testBootLoopLevels() {
+        // this is old test where the flag needs to be disabled
+        mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
         RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
 
         assertEquals(observer.onBootLoop(0), PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
index 599b9cd..8e1e339 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
@@ -273,10 +273,8 @@
     keyValueMap.put("namespace_1*flag_1", "true");
     // case 2: existing prop, stage a different value
     keyValueMap.put("namespace_1*flag_2", "false");
-    // case 3: new prop, stage the non default value
+    // case 3: new prop
     keyValueMap.put("namespace_2*flag_1", "true");
-    // case 4: new prop, stage the default value
-    keyValueMap.put("namespace_2*flag_2", "false");
     Properties props = new Properties(namespace, keyValueMap);
 
     HashMap<String, HashMap<String, String>> toStageProps =
@@ -290,11 +288,9 @@
     String namespace_1_flag_1 = namespace_1_to_stage.get("flag_1");
     String namespace_1_flag_2 = namespace_1_to_stage.get("flag_2");
     String namespace_2_flag_1 = namespace_2_to_stage.get("flag_1");
-    String namespace_2_flag_2 = namespace_2_to_stage.get("flag_2");
     Assert.assertTrue(namespace_1_flag_1 == null);
     Assert.assertTrue(namespace_1_flag_2 != null);
     Assert.assertTrue(namespace_2_flag_1 != null);
-    Assert.assertTrue(namespace_2_flag_2 == null);
     Assert.assertTrue(namespace_1_flag_2.equals("false"));
     Assert.assertTrue(namespace_2_flag_1.equals("true"));
   }
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 11f20e3..d15c24b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -31,6 +31,7 @@
 import static com.android.server.job.JobSchedulerService.sUptimeMillisClock;
 import static com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS;
 import static com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK;
+import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -74,6 +75,9 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+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 com.android.server.AppStateTracker;
@@ -85,6 +89,8 @@
 import com.android.server.job.controllers.ConnectivityController;
 import com.android.server.job.controllers.JobStatus;
 import com.android.server.job.controllers.QuotaController;
+import com.android.server.job.restrictions.JobRestriction;
+import com.android.server.job.restrictions.ThermalStatusRestriction;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.usage.AppStandbyInternal;
 
@@ -121,6 +127,9 @@
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     private ChargingPolicyChangeListener mChargingPolicyChangeListener;
 
     private class TestJobSchedulerService extends JobSchedulerService {
@@ -2385,6 +2394,108 @@
         assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job2b));
     }
 
+    /**
+     * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with single {@link
+     * JobRestriction} registered.
+     */
+    @Test
+    public void testCheckIfRestrictedSingleRestriction() {
+        int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE;
+        JobStatus fgsJob =
+                createJobStatus(
+                        "testCheckIfRestrictedSingleRestriction", createJobInfo(1).setBias(bias));
+        ThermalStatusRestriction mockThermalStatusRestriction =
+                mock(ThermalStatusRestriction.class);
+        mService.mJobRestrictions.clear();
+        mService.mJobRestrictions.add(mockThermalStatusRestriction);
+        when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+
+        synchronized (mService.mLock) {
+            assertEquals(mService.checkIfRestricted(fgsJob), mockThermalStatusRestriction);
+        }
+
+        when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+        synchronized (mService.mLock) {
+            assertNull(mService.checkIfRestricted(fgsJob));
+        }
+    }
+
+    /**
+     * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with multiple {@link
+     * JobRestriction} registered.
+     */
+    @Test
+    public void testCheckIfRestrictedMultipleRestrictions() {
+        int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE;
+        JobStatus fgsJob =
+                createJobStatus(
+                        "testGetMinJobExecutionGuaranteeMs", createJobInfo(1).setBias(bias));
+        JobRestriction mock1JobRestriction = mock(JobRestriction.class);
+        JobRestriction mock2JobRestriction = mock(JobRestriction.class);
+        mService.mJobRestrictions.clear();
+        mService.mJobRestrictions.add(mock1JobRestriction);
+        mService.mJobRestrictions.add(mock2JobRestriction);
+
+        // Jobs will be restricted if any one of the registered {@link JobRestriction}
+        // reports true.
+        when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+        when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+        synchronized (mService.mLock) {
+            assertEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction);
+        }
+
+        when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+        when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+        synchronized (mService.mLock) {
+            assertEquals(mService.checkIfRestricted(fgsJob), mock2JobRestriction);
+        }
+
+        when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+        when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+        synchronized (mService.mLock) {
+            assertNull(mService.checkIfRestricted(fgsJob));
+        }
+
+        when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+        when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+        synchronized (mService.mLock) {
+            assertNotEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction);
+        }
+    }
+
+    /**
+     * Jobs with foreground service and top app biases must not be restricted when the flag is
+     * disabled.
+     */
+    @Test
+    @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+    public void testCheckIfRestricted_highJobBias_flagThermalRestrictionsToFgsJobsDisabled() {
+        JobStatus fgsJob =
+                createJobStatus(
+                        "testCheckIfRestrictedJobBiasFgs",
+                        createJobInfo(1).setBias(JobInfo.BIAS_FOREGROUND_SERVICE));
+        JobStatus topAppJob =
+                createJobStatus(
+                        "testCheckIfRestrictedJobBiasTopApp",
+                        createJobInfo(2).setBias(JobInfo.BIAS_TOP_APP));
+
+        synchronized (mService.mLock) {
+            assertNull(mService.checkIfRestricted(fgsJob));
+            assertNull(mService.checkIfRestricted(topAppJob));
+        }
+    }
+
+    /** Jobs with top app biases must not be restricted. */
+    @Test
+    public void testCheckIfRestricted_highJobBias() {
+        JobStatus topAppJob = createJobStatus(
+                "testCheckIfRestrictedJobBiasTopApp",
+                createJobInfo(1).setBias(JobInfo.BIAS_TOP_APP));
+        synchronized (mService.mLock) {
+            assertNull(mService.checkIfRestricted(topAppJob));
+        }
+    }
+
     private void setBatteryLevel(int level) {
         doReturn(level).when(mBatteryManagerInternal).getBatteryLevel();
         mService.mBatteryStateTracker
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
index 754f409..c2c67e6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
@@ -28,6 +28,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -43,7 +44,12 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.PowerManager;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.DeviceConfig;
+import android.util.DebugUtils;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -53,6 +59,7 @@
 
 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;
@@ -76,6 +83,157 @@
     @Mock
     private JobSchedulerService mJobSchedulerService;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    class JobStatusContainer {
+        public final JobStatus jobMinPriority;
+        public final JobStatus jobLowPriority;
+        public final JobStatus jobLowPriorityRunning;
+        public final JobStatus jobLowPriorityRunningLong;
+        public final JobStatus jobDefaultPriority;
+        public final JobStatus jobHighPriority;
+        public final JobStatus jobHighPriorityRunning;
+        public final JobStatus jobHighPriorityRunningLong;
+        public final JobStatus ejDowngraded;
+        public final JobStatus ej;
+        public final JobStatus ejRetried;
+        public final JobStatus ejRunning;
+        public final JobStatus ejRunningLong;
+        public final JobStatus ui;
+        public final JobStatus uiRetried;
+        public final JobStatus uiRunning;
+        public final JobStatus uiRunningLong;
+        public final JobStatus importantWhileForeground;
+        public final JobStatus importantWhileForegroundRunning;
+        public final JobStatus importantWhileForegroundRunningLong;
+        public final int[] allJobBiases = {
+            JobInfo.BIAS_ADJ_ALWAYS_RUNNING,
+            JobInfo.BIAS_ADJ_OFTEN_RUNNING,
+            JobInfo.BIAS_DEFAULT,
+            JobInfo.BIAS_SYNC_EXPEDITED,
+            JobInfo.BIAS_SYNC_INITIALIZATION,
+            JobInfo.BIAS_BOUND_FOREGROUND_SERVICE,
+            JobInfo.BIAS_FOREGROUND_SERVICE,
+            JobInfo.BIAS_TOP_APP
+        };
+        public final int[] biasesBelowFgs = {
+            JobInfo.BIAS_ADJ_ALWAYS_RUNNING,
+            JobInfo.BIAS_ADJ_OFTEN_RUNNING,
+            JobInfo.BIAS_DEFAULT,
+            JobInfo.BIAS_SYNC_EXPEDITED,
+            JobInfo.BIAS_SYNC_INITIALIZATION,
+            JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+        };
+        public final int[] thermalStatuses = {
+            THERMAL_STATUS_NONE,
+            THERMAL_STATUS_LIGHT,
+            THERMAL_STATUS_MODERATE,
+            THERMAL_STATUS_SEVERE,
+            THERMAL_STATUS_CRITICAL,
+            THERMAL_STATUS_EMERGENCY,
+            THERMAL_STATUS_SHUTDOWN
+        };
+
+        JobStatusContainer(String jobName, JobSchedulerService mJobSchedulerService) {
+            jobMinPriority =
+                    createJobStatus(
+                            jobName, createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build());
+            jobLowPriority =
+                    createJobStatus(
+                            jobName, createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build());
+            jobLowPriorityRunning =
+                    createJobStatus(
+                            jobName, createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build());
+            jobLowPriorityRunningLong =
+                    createJobStatus(
+                            jobName, createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build());
+            jobDefaultPriority =
+                    createJobStatus(
+                            jobName,
+                            createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build());
+            jobHighPriority =
+                    createJobStatus(
+                            jobName,
+                            createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build());
+            jobHighPriorityRunning =
+                    createJobStatus(
+                            jobName,
+                            createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build());
+            jobHighPriorityRunningLong =
+                    createJobStatus(
+                            jobName,
+                            createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build());
+            ejDowngraded = createJobStatus(jobName, createJobBuilder(7).setExpedited(true).build());
+            ej = spy(createJobStatus(jobName, createJobBuilder(8).setExpedited(true).build()));
+            ejRetried =
+                    spy(createJobStatus(jobName, createJobBuilder(11).setExpedited(true).build()));
+            ejRunning =
+                    spy(createJobStatus(jobName, createJobBuilder(12).setExpedited(true).build()));
+            ejRunningLong =
+                    spy(createJobStatus(jobName, createJobBuilder(13).setExpedited(true).build()));
+            ui = spy(createJobStatus(jobName, createJobBuilder(14).build()));
+            uiRetried = spy(createJobStatus(jobName, createJobBuilder(15).build()));
+            uiRunning = spy(createJobStatus(jobName, createJobBuilder(16).build()));
+            uiRunningLong = spy(createJobStatus(jobName, createJobBuilder(17).build()));
+            importantWhileForeground = spy(createJobStatus(jobName, createJobBuilder(18)
+                    .setImportantWhileForeground(true)
+                    .build()));
+            importantWhileForegroundRunning = spy(createJobStatus(jobName, createJobBuilder(20)
+                    .setImportantWhileForeground(true)
+                     .build()));
+            importantWhileForegroundRunningLong = spy(createJobStatus(jobName, createJobBuilder(19)
+                     .setImportantWhileForeground(true)
+                     .build()));
+
+            when(ej.shouldTreatAsExpeditedJob()).thenReturn(true);
+            when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true);
+            when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true);
+            when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true);
+            when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+            when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+            when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+            when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+            when(ejRetried.getNumPreviousAttempts()).thenReturn(1);
+            when(uiRetried.getNumPreviousAttempts()).thenReturn(2);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true);
+            when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunning))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true);
+            when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true);
+            when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunningLong))
+                    .thenReturn(true);
+            when(mJobSchedulerService.isJobInOvertimeLocked(importantWhileForegroundRunningLong))
+                    .thenReturn(true);
+        }
+    }
+
+    private boolean isJobRestricted(JobStatus status, int bias) {
+        return mThermalStatusRestriction.isJobRestricted(status, bias);
+    }
+
+    private static String debugTag(int bias, @PowerManager.ThermalStatus int status) {
+        return "Bias = "
+                + JobInfo.getBiasString(bias)
+                + " Thermal Status = "
+                + DebugUtils.valueToString(PowerManager.class, "THERMAL_STATUS_", status);
+    }
+
     @Before
     public void setUp() {
         mMockingSession = mockitoSession()
@@ -156,169 +314,302 @@
         assertEquals(THERMAL_STATUS_EMERGENCY, mThermalStatusRestriction.getThermalStatus());
     }
 
+    /**
+     * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Thermal is in default state
+     */
     @Test
-    public void testIsJobRestricted() {
+    public void testIsJobRestrictedDefaultStates() {
         mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE);
+        JobStatusContainer jc = new JobStatusContainer("testIsJobRestricted", mJobSchedulerService);
 
-        final JobStatus jobMinPriority = createJobStatus("testIsJobRestricted",
-                createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build());
-        final JobStatus jobLowPriority = createJobStatus("testIsJobRestricted",
-                createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build());
-        final JobStatus jobLowPriorityRunning = createJobStatus("testIsJobRestricted",
-                createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build());
-        final JobStatus jobLowPriorityRunningLong = createJobStatus("testIsJobRestricted",
-                createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build());
-        final JobStatus jobDefaultPriority = createJobStatus("testIsJobRestricted",
-                createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build());
-        final JobStatus jobHighPriority = createJobStatus("testIsJobRestricted",
-                createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build());
-        final JobStatus jobHighPriorityRunning = createJobStatus("testIsJobRestricted",
-                createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build());
-        final JobStatus jobHighPriorityRunningLong = createJobStatus("testIsJobRestricted",
-                createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build());
-        final JobStatus ejDowngraded = createJobStatus("testIsJobRestricted",
-                createJobBuilder(7).setExpedited(true).build());
-        final JobStatus ej = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(8).setExpedited(true).build()));
-        final JobStatus ejRetried = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(11).setExpedited(true).build()));
-        final JobStatus ejRunning = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(12).setExpedited(true).build()));
-        final JobStatus ejRunningLong = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(13).setExpedited(true).build()));
-        final JobStatus ui = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(14).build()));
-        final JobStatus uiRetried = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(15).build()));
-        final JobStatus uiRunning = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(16).build()));
-        final JobStatus uiRunningLong = spy(createJobStatus("testIsJobRestricted",
-                createJobBuilder(17).build()));
-        when(ej.shouldTreatAsExpeditedJob()).thenReturn(true);
-        when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true);
-        when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true);
-        when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true);
-        when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true);
-        when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true);
-        when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true);
-        when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true);
-        when(ejRetried.getNumPreviousAttempts()).thenReturn(1);
-        when(uiRetried.getNumPreviousAttempts()).thenReturn(2);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
-                .thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong))
-                .thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong))
-                .thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true);
-        when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true);
-        when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
-                .thenReturn(true);
-        when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
-                .thenReturn(true);
-        when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true);
-        when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true);
+        for (int jobBias : jc.allJobBiases) {
+            assertFalse(isJobRestricted(jc.jobMinPriority, jobBias));
+            assertFalse(isJobRestricted(jc.jobLowPriority, jobBias));
+            assertFalse(isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+            assertFalse(isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+            assertFalse(isJobRestricted(jc.jobDefaultPriority, jobBias));
+            assertFalse(isJobRestricted(jc.jobHighPriority, jobBias));
+            assertFalse(isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+            assertFalse(isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+            assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias));
+            assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+            assertFalse(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+            assertFalse(isJobRestricted(jc.ej, jobBias));
+            assertFalse(isJobRestricted(jc.ejDowngraded, jobBias));
+            assertFalse(isJobRestricted(jc.ejRetried, jobBias));
+            assertFalse(isJobRestricted(jc.ejRunning, jobBias));
+            assertFalse(isJobRestricted(jc.ejRunningLong, jobBias));
+            assertFalse(isJobRestricted(jc.ui, jobBias));
+            assertFalse(isJobRestricted(jc.uiRetried, jobBias));
+            assertFalse(isJobRestricted(jc.uiRunning, jobBias));
+            assertFalse(isJobRestricted(jc.uiRunningLong, jobBias));
+        }
+    }
 
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+    /**
+     * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Top App and all
+     * Thermal states.
+     */
+    @Test
+    public void testIsJobRestrictedBiasTopApp() {
+        JobStatusContainer jc =
+                new JobStatusContainer("testIsJobRestrictedBiasTopApp", mJobSchedulerService);
 
-        mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_LIGHT);
+        int jobBias = JobInfo.BIAS_TOP_APP;
+        for (int thermalStatus : jc.thermalStatuses) {
+            String msg = "Thermal Status = " + DebugUtils.valueToString(
+                    PowerManager.class, "THERMAL_STATUS_", thermalStatus);
+            mStatusChangedListener.onThermalStatusChanged(thermalStatus);
 
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+            // No restrictions on any jobs
+            assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias));
+            assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+            assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+            assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+        }
+    }
 
-        mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_MODERATE);
+    /**
+     * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground
+     * Service and all Thermal states.
+     */
+    @Test
+    @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+    public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsDisabled() {
+        JobStatusContainer jc =
+                new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService);
 
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
-        assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+        int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE;
+        for (int thermalStatus : jc.thermalStatuses) {
+            String msg = "Thermal Status = " + DebugUtils.valueToString(
+                    PowerManager.class, "THERMAL_STATUS_", thermalStatus);
+            mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+            // No restrictions on any jobs
+            assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+            assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+            assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+            assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+            assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+        }
+    }
 
-        mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SEVERE);
+    /**
+     * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground
+     * Service and all Thermal states.
+     */
+    @Test
+    @RequiresFlagsEnabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+    public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsEnabled() {
+        JobStatusContainer jc =
+                new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService);
+        int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE;
+        for (int thermalStatus : jc.thermalStatuses) {
+            String msg = debugTag(jobBias, thermalStatus);
+            mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+            if (thermalStatus >= THERMAL_STATUS_SEVERE) {
+                // Full restrictions on all jobs
+                assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ej, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ui, jobBias));
+                assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias));
+                assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+            } else if (thermalStatus >= THERMAL_STATUS_MODERATE) {
+                // No restrictions on user related jobs
+                assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+                assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+                assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+                assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+                // Some restrictions on expedited jobs
+                assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+                assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                // Some restrictions on high priority jobs
+                assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                // Some restructions on important while foreground jobs
+                assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias));
+                assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+                assertTrue(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+                // Full restriction on default priority jobs
+                assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                // Full restriction on low priority jobs
+                assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                // Full restriction on min priority jobs
+                assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+            } else {
+                // thermalStatus < THERMAL_STATUS_MODERATE
+                // No restrictions on any job type
+                assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+                assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+                assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+                assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+                assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+                assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+                assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+                assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias));
+                assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+                assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+            }
+        }
+    }
 
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ej));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ui));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+    /**
+     * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is less than
+     * Foreground Service and all Thermal states.
+     */
+    @Test
+    public void testIsJobRestrictedBiasLessThanFgs() {
+        JobStatusContainer jc =
+                new JobStatusContainer("testIsJobRestrictedBiasLessThanFgs", mJobSchedulerService);
 
-        mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_CRITICAL);
-
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ej));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(ui));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning));
-        assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+        for (int jobBias : jc.biasesBelowFgs) {
+            for (int thermalStatus : jc.thermalStatuses) {
+                String msg = debugTag(jobBias, thermalStatus);
+                mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+                if (thermalStatus >= THERMAL_STATUS_SEVERE) {
+                    // Full restrictions on all jobs
+                    assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ej, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ui, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+                } else if (thermalStatus >= THERMAL_STATUS_MODERATE) {
+                    // No restrictions on user related jobs
+                    assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+                    // Some restrictions on expedited jobs
+                    assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                    // Some restrictions on high priority jobs
+                    assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                    // Full restriction on default priority jobs
+                    assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                    // Full restriction on low priority jobs
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                    // Full restriction on min priority jobs
+                    assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+                } else if (thermalStatus >= THERMAL_STATUS_LIGHT) {
+                    // No restrictions on any user related jobs
+                    assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+                    // No restrictions on any expedited jobs
+                    assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                    // No restrictions on any high priority jobs
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                    // No restrictions on default priority jobs
+                    assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                    // Some restrictions on low priority jobs
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                    assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                    // Full restriction on min priority jobs
+                    assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+                } else { // THERMAL_STATUS_NONE
+                    // No restrictions on any jobs
+                    assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+                    assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+                }
+            }
+        }
     }
 
     private JobInfo.Builder createJobBuilder(int jobId) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 79f1574..7d58a2e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -778,6 +778,49 @@
     }
 
     @Test
+    public void testGetAliveUsers_shouldExcludeInitialisedEphemeralNonCurrentUsers() {
+        assertWithMessage("Ephemeral user should not exist at all initially")
+                .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID))
+                .isFalse();
+
+        // add an ephemeral full user
+        TestUserData userData = new TestUserData(USER_ID);
+        userData.info.flags = UserInfo.FLAG_FULL | UserInfo.FLAG_EPHEMERAL;
+        addUserData(userData);
+
+        assertWithMessage("Ephemeral user should exist as alive after being created")
+                .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID))
+                .isTrue();
+
+        // mock switch to the user (mark it as initialized & make it the current user)
+        userData.info.flags |= UserInfo.FLAG_INITIALIZED;
+        mockCurrentUser(USER_ID);
+
+        assertWithMessage("Ephemeral user should still exist as alive after being switched to")
+                .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID))
+                .isTrue();
+
+        // switch away from the user
+        mockCurrentUser(OTHER_USER_ID);
+
+        assertWithMessage("Ephemeral user should not exist as alive after getting switched away")
+                .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID))
+                .isFalse();
+
+        assertWithMessage("Ephemeral user should still exist as dying after getting switched away")
+                .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID))
+                .isTrue();
+
+        // finally remove the user
+        mUms.removeUserInfo(USER_ID);
+
+        assertWithMessage("Ephemeral user should not exist at all after cleanup")
+                .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID))
+                .isFalse();
+    }
+
+
+    @Test
     @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
             Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES})
     public void testCreatePrivateProfileOnHeadlessSystemUser_shouldAllowCreation() {
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java
new file mode 100644
index 0000000..7f2327aa
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import static android.app.AppOpsManager.OP_CAMERA;
+import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.content.AttributionSource;
+import android.os.Process;
+import android.permission.PermissionManager;
+import android.permission.flags.Flags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.testing.TestableContext;
+import android.virtualdevice.cts.common.VirtualDeviceRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+public class AppOpsDeviceAwareServiceTest {
+
+    @Rule
+    public final TestableContext mContext =
+            new TestableContext(InstrumentationRegistry.getInstrumentation().getTargetContext());
+
+    @Rule
+    public VirtualDeviceRule virtualDeviceRule =
+            VirtualDeviceRule.withAdditionalPermissions(
+                    Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
+                    Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
+                    Manifest.permission.CREATE_VIRTUAL_DEVICE,
+                    Manifest.permission.GET_APP_OPS_STATS);
+
+    private static final String ATTRIBUTION_TAG_1 = "attributionTag1";
+    private static final String ATTRIBUTION_TAG_2 = "attributionTag2";
+    private final AppOpsManager mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+    private final PermissionManager mPermissionManager =
+            mContext.getSystemService(PermissionManager.class);
+
+    private VirtualDeviceManager.VirtualDevice mVirtualDevice;
+
+    @Before
+    public void setUp() {
+        mVirtualDevice =
+                virtualDeviceRule.createManagedVirtualDevice(
+                        new VirtualDeviceParams.Builder()
+                                .setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM)
+                                .build());
+
+        mPermissionManager.grantRuntimePermission(
+                mContext.getOpPackageName(),
+                Manifest.permission.CAMERA,
+                mVirtualDevice.getPersistentDeviceId());
+
+        mPermissionManager.grantRuntimePermission(
+                mContext.getOpPackageName(),
+                Manifest.permission.CAMERA,
+                VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_DEVICE_ID_IN_OP_PROXY_INFO_ENABLED)
+    @Test
+    public void noteProxyOp_proxyAppOnDefaultDevice() {
+        AppOpsManager.OpEventProxyInfo proxyInfo =
+                noteProxyOpWithDeviceId(TestableContext.DEVICE_ID_DEFAULT);
+        assertThat(proxyInfo.getDeviceId())
+                .isEqualTo(VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_DEVICE_ID_IN_OP_PROXY_INFO_ENABLED)
+    @Test
+    public void noteProxyOp_proxyAppOnRemoteDevice() {
+        AppOpsManager.OpEventProxyInfo proxyInfo =
+                noteProxyOpWithDeviceId(mVirtualDevice.getDeviceId());
+        assertThat(proxyInfo.getDeviceId()).isEqualTo(mVirtualDevice.getPersistentDeviceId());
+    }
+
+    private AppOpsManager.OpEventProxyInfo noteProxyOpWithDeviceId(int proxyAppDeviceId) {
+        AttributionSource proxiedAttributionSource =
+                new AttributionSource.Builder(Process.myUid())
+                        .setPackageName(mContext.getOpPackageName())
+                        .setAttributionTag(ATTRIBUTION_TAG_2)
+                        .setDeviceId(mVirtualDevice.getDeviceId())
+                        .build();
+
+        AttributionSource proxyAttributionSource =
+                new AttributionSource.Builder(Process.myUid())
+                        .setPackageName(mContext.getOpPackageName())
+                        .setAttributionTag(ATTRIBUTION_TAG_1)
+                        .setDeviceId(proxyAppDeviceId)
+                        .setNextAttributionSource(proxiedAttributionSource)
+                        .build();
+
+        int mode = mAppOpsManager.noteProxyOp(OP_CAMERA, proxyAttributionSource, null, false);
+        assertThat(mode).isEqualTo(AppOpsManager.MODE_ALLOWED);
+
+        List<AppOpsManager.PackageOps> packagesOps =
+                mAppOpsManager.getPackagesForOps(
+                        new String[] {AppOpsManager.OPSTR_CAMERA},
+                        mVirtualDevice.getPersistentDeviceId());
+
+        AppOpsManager.PackageOps packageOps =
+                packagesOps.stream()
+                        .filter(
+                                pkg ->
+                                        Objects.equals(
+                                                pkg.getPackageName(), mContext.getOpPackageName()))
+                        .findFirst()
+                        .orElseThrow();
+
+        AppOpsManager.OpEntry opEntry =
+                packageOps.getOps().stream()
+                        .filter(op -> op.getOp() == OP_CAMERA)
+                        .findFirst()
+                        .orElseThrow();
+
+        return opEntry.getLastProxyInfo(OP_FLAGS_ALL_TRUSTED);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java
new file mode 100644
index 0000000..3698d6f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.biometrics;
+
+import static com.android.server.biometrics.sensors.BiometricNotificationUtils.NOTIFICATION_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricsProtoEnums;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BiometricDanglingReceiverTest {
+    @Rule
+    public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+    private BiometricDanglingReceiver mBiometricDanglingReceiver;
+
+    @Rule
+    public final TestableContext mContext = spy(new TestableContext(
+            InstrumentationRegistry.getInstrumentation().getTargetContext(), null));
+
+    @Mock
+    NotificationManager mNotificationManager;
+
+    @Mock
+    Intent mIntent;
+
+    @Captor
+    private ArgumentCaptor<Intent> mArgumentCaptor;
+
+    @Before
+    public void setUp() {
+        mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
+    }
+
+    @Test
+    public void testFingerprintRegisterReceiver() {
+        initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+        verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any(),
+                eq(Context.RECEIVER_NOT_EXPORTED));
+    }
+
+    @Test
+    public void testFaceRegisterReceiver() {
+        initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE);
+        verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any(),
+                eq(Context.RECEIVER_NOT_EXPORTED));
+    }
+
+    @Test
+    public void testOnReceive_fingerprintReEnrollLaunch() {
+        initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+        when(mIntent.getAction()).thenReturn(
+                BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+
+        mBiometricDanglingReceiver.onReceive(mContext, mIntent);
+
+        // Verify fingerprint enroll process is launched.
+        verify(mContext).startActivity(mArgumentCaptor.capture());
+        assertThat(mArgumentCaptor.getValue().getAction())
+                .isEqualTo(Settings.ACTION_FINGERPRINT_ENROLL);
+
+        // Verify notification is canceled
+        verify(mNotificationManager).cancelAsUser("FingerprintReEnroll", NOTIFICATION_ID,
+                UserHandle.CURRENT);
+
+        // Verify receiver is unregistered after receiving the broadcast
+        verify(mContext).unregisterReceiver(mBiometricDanglingReceiver);
+    }
+
+    @Test
+    public void testOnReceive_faceReEnrollLaunch() {
+        initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE);
+        when(mIntent.getAction()).thenReturn(
+                BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
+
+        mBiometricDanglingReceiver.onReceive(mContext, mIntent);
+
+        // Verify face enroll process is launched.
+        verify(mContext).startActivity(mArgumentCaptor.capture());
+        assertThat(mArgumentCaptor.getValue().getAction())
+                .isEqualTo(BiometricDanglingReceiver.FACE_SETTINGS_ACTION);
+
+        // Verify notification is canceled
+        verify(mNotificationManager).cancelAsUser("FaceReEnroll", NOTIFICATION_ID,
+                UserHandle.CURRENT);
+
+        // Verify receiver is unregistered after receiving the broadcast.
+        verify(mContext).unregisterReceiver(mBiometricDanglingReceiver);
+    }
+
+    private void initBroadcastReceiver(int modality) {
+        mBiometricDanglingReceiver = new BiometricDanglingReceiver(mContext, modality);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index fc573d2..3789531 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -1228,6 +1228,11 @@
             Slog.d(TAG, "TestInternalEnumerateClient#startHalOperation");
             onEnumerationResult(TEST_FINGERPRINT, 0 /* remaining */);
         }
+
+        @Override
+        protected int getModality() {
+            return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+        }
     }
 
     private static class TestRemovalClient extends RemovalClient<Fingerprint, Object> {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
index 9845b58..d8bdd50 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
@@ -20,8 +20,10 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -79,15 +81,21 @@
     private final int mBiometricId = 1;
     private final Face mFace = new Face("face", mBiometricId, 1 /* deviceId */);
     private FaceInternalEnumerateClient mClient;
+    private boolean mNotificationSent;
 
     @Before
     public void setUp() {
         when(mAidlSession.getSession()).thenReturn(mSession);
-
         final List<Face> enrolled = new ArrayList<>();
         enrolled.add(mFace);
-        mClient = new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID,
-                TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext);
+        mClient = spy(new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID,
+                TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext));
+
+        mNotificationSent = false;
+        doAnswer(invocation -> {
+            mNotificationSent = true;
+            return null;
+        }).when(mClient).sendDanglingNotification(anyList());
     }
 
     @Test
@@ -101,6 +109,7 @@
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+        assertThat(mNotificationSent).isFalse();
         verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
         verify(mCallback).onClientFinished(mClient, true);
     }
@@ -116,6 +125,7 @@
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+        assertThat(mNotificationSent).isFalse();
         verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
         verify(mCallback, never()).onClientFinished(mClient, true);
     }
@@ -131,6 +141,7 @@
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+        assertThat(mNotificationSent).isTrue();
         verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
         verify(mCallback).onClientFinished(mClient, true);
     }
@@ -147,6 +158,7 @@
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
+        assertThat(mNotificationSent).isFalse();
         verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
         verify(mCallback, never()).onClientFinished(mClient, true);
     }
@@ -164,6 +176,7 @@
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
+        assertThat(mNotificationSent).isTrue();
         verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
         verify(mCallback).onClientFinished(mClient, true);
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
index b5df836..fab1200 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
@@ -20,8 +20,10 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -80,15 +82,23 @@
 
     private FingerprintInternalEnumerateClient mClient;
 
+    private boolean mNotificationSent;
+
     @Before
     public void setUp() {
         when(mAidlSession.getSession()).thenReturn(mSession);
 
         List<Fingerprint> enrolled = new ArrayList<>();
         enrolled.add(new Fingerprint("one", 1, 1));
-        mClient = new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken,
+        mClient = spy(new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken,
                 USER_ID, TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger,
-                mBiometricContext);
+                mBiometricContext));
+
+        mNotificationSent = false;
+        doAnswer(invocation -> {
+            mNotificationSent = true;
+            return null;
+        }).when(mClient).sendDanglingNotification(anyList());
     }
 
     @Test
@@ -104,6 +114,7 @@
         assertThat(mClient.getUnknownHALTemplates().stream()
                 .flatMap(x -> Stream.of(x.getBiometricId()))
                 .collect(Collectors.toList())).containsExactly(2, 3);
+        assertThat(mNotificationSent).isTrue();
         verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, 1);
         verify(mCallback).onClientFinished(mClient, true);
     }
@@ -118,6 +129,7 @@
 
         verify(mSession).enumerateEnrollments();
         assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+        assertThat(mNotificationSent).isFalse();
         verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
         verify(mCallback).onClientFinished(mClient, true);
     }
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index fd880dd..178e7ec 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -33,7 +33,6 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.DisplayInfo;
@@ -41,13 +40,11 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.input.flags.Flags;
 import com.android.server.LocalServices;
 import com.android.server.input.InputManagerInternal;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -60,9 +57,6 @@
     private static final String LANGUAGE_TAG = "en-US";
     private static final String LAYOUT_TYPE = "qwerty";
 
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
     @Mock
     private InputManagerInternal mInputManagerInternalMock;
     @Mock
@@ -77,8 +71,6 @@
 
     @Before
     public void setUp() throws Exception {
-        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER);
-
         MockitoAnnotations.initMocks(this);
         mInputManagerMockHelper = new InputManagerMockHelper(
                 TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
index b33a8aa..00c8ed1 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputManagerMockHelper.java
@@ -53,7 +53,7 @@
     private IInputDevicesChangedListener mDevicesChangedListener;
     private final Map<String /* uniqueId */, Integer /* displayId */> mDisplayIdMapping =
             new HashMap<>();
-    private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociation =
+    private final Map<String /* phys */, String /* uniqueId */> mUniqueIdAssociationByPort =
             new HashMap<>();
 
     InputManagerMockHelper(TestableLooper testableLooper,
@@ -79,11 +79,11 @@
         when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[0]);
         doAnswer(inv -> mDevices.get(inv.getArgument(0)))
                 .when(mIInputManagerMock).getInputDevice(anyInt());
-        doAnswer(inv -> mUniqueIdAssociation.put(inv.getArgument(0),
-                inv.getArgument(1))).when(mIInputManagerMock).addUniqueIdAssociation(
+        doAnswer(inv -> mUniqueIdAssociationByPort.put(inv.getArgument(0),
+                inv.getArgument(1))).when(mIInputManagerMock).addUniqueIdAssociationByPort(
                         anyString(), anyString());
-        doAnswer(inv -> mUniqueIdAssociation.remove(inv.getArgument(0))).when(
-                mIInputManagerMock).removeUniqueIdAssociation(anyString());
+        doAnswer(inv -> mUniqueIdAssociationByPort.remove(inv.getArgument(0))).when(
+                mIInputManagerMock).removeUniqueIdAssociationByPort(anyString());
 
         // Set a new instance of InputManager for testing that uses the IInputManager mock as the
         // interface to the server.
@@ -113,7 +113,7 @@
                 .setDescriptor(phys)
                 .setExternal(true)
                 .setAssociatedDisplayId(
-                        mDisplayIdMapping.getOrDefault(mUniqueIdAssociation.get(phys),
+                        mDisplayIdMapping.getOrDefault(mUniqueIdAssociationByPort.get(phys),
                                 Display.INVALID_DISPLAY))
                 .build();
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 2b81d78..da8961d 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -339,8 +339,6 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
-        mSetFlagsRule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER);
-
         doNothing().when(mInputManagerInternalMock)
                 .setMousePointerAccelerationEnabled(anyBoolean(), anyInt());
         doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 40ecaf1..7dd1847 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -42,6 +42,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.HandlerThread;
 import android.os.LocaleList;
 import android.os.Process;
@@ -488,7 +489,7 @@
 
         setUpPackageInstalled(pkgNameA);
 
-        mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+        mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, new Bundle());
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
                 LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
@@ -504,7 +505,7 @@
 
         setUpPackageInstalled(pkgNameB);
 
-        mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+        mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, new Bundle());
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
                 LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
@@ -518,6 +519,66 @@
     }
 
     @Test
+    public void testRestore_appInstalledAfterSUW_restoresFromStage_ArchiveEnabled()
+            throws Exception {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
+        String pkgNameA = "com.android.myAppA";
+        String pkgNameB = "com.android.myAppB";
+        String langTagsA = "ru";
+        String langTagsB = "hi,fr";
+        LocalesInfo localesInfoA = new LocalesInfo(langTagsA, false);
+        LocalesInfo localesInfoB = new LocalesInfo(langTagsB, true);
+        pkgLocalesMap.put(pkgNameA, localesInfoA);
+        pkgLocalesMap.put(pkgNameB, localesInfoB);
+        writeTestPayload(out, pkgLocalesMap);
+        setUpPackageNotInstalled(pkgNameA);
+        setUpPackageNotInstalled(pkgNameB);
+        setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
+        setUpLocalesForPackage(pkgNameB, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>());
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(Intent.EXTRA_ARCHIVAL, true);
+        mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, bundle);
+        mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, bundle);
+
+        mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+        verifyNothingRestored();
+
+        setUpPackageInstalled(pkgNameA);
+
+        mPackageMonitor.onPackageUpdateFinished(pkgNameA, DEFAULT_UID);
+
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
+                LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
+                .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false);
+
+        verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+
+        pkgLocalesMap.remove(pkgNameA);
+
+        verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+
+        setUpPackageInstalled(pkgNameB);
+
+        mPackageMonitor.onPackageUpdateFinished(pkgNameB, DEFAULT_UID);
+
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
+                LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
+                .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false);
+
+        verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID),
+            new ArraySet<>(Arrays.asList(pkgNameB)));
+        checkStageDataDoesNotExist(DEFAULT_USER_ID);
+    }
+
+    @Test
     public void testRestore_appInstalledAfterSUWAndLocalesAlreadySet_restoresNothing()
             throws Exception {
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -535,7 +596,7 @@
         setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
         setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.forLanguageTags("hi,mr"));
 
-        mPackageMonitor.onPackageAdded(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+        mPackageMonitor.onPackageAddedWithExtras(DEFAULT_PACKAGE_NAME, DEFAULT_UID, new Bundle());
 
         // Since locales are already set, we should not restore anything for it.
         verifyNothingRestored();
@@ -612,7 +673,7 @@
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
         setUpPackageInstalled(pkgNameA);
 
-        mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+        mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, new Bundle());
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(
                 pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false,
@@ -627,7 +688,7 @@
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
         setUpPackageInstalled(pkgNameB);
 
-        mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+        mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, new Bundle());
 
         verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(),
                 any(), anyBoolean(), anyInt());
diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
index 5902caa..d0acacc 100644
--- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java
@@ -26,12 +26,15 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -39,9 +42,12 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.content.Context;
+import android.hardware.power.ChannelConfig;
+import android.hardware.power.IPower;
 import android.hardware.power.SessionConfig;
 import android.hardware.power.SessionTag;
 import android.hardware.power.WorkDuration;
@@ -50,6 +56,7 @@
 import android.os.IHintSession;
 import android.os.PerformanceHintManager;
 import android.os.Process;
+import android.os.RemoteException;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -130,12 +137,15 @@
     @Mock
     private HintManagerService.NativeWrapper mNativeWrapperMock;
     @Mock
+    private IPower mIPowerMock;
+    @Mock
     private ActivityManagerInternal mAmInternalMock;
     @Rule
     public final CheckFlagsRule mCheckFlagsRule =
             DeviceFlagsValueProvider.createCheckFlagsRule();
 
     private HintManagerService mService;
+    private ChannelConfig mConfig;
 
     private static Answer<Long> fakeCreateWithConfig(Long ptr, Long sessionId) {
         return new Answer<Long>() {
@@ -149,6 +159,9 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        mConfig = new ChannelConfig();
+        mConfig.readFlagBitmask = 1;
+        mConfig.writeFlagBitmask = 2;
         when(mNativeWrapperMock.halGetHintSessionPreferredRate())
                 .thenReturn(DEFAULT_HINT_PREFERRED_RATE);
         when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A),
@@ -170,6 +183,8 @@
                 any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[2],
                     SESSION_IDS[2]));
 
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(5);
+        when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);
         LocalServices.removeServiceForTest(ActivityManagerInternal.class);
         LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock);
     }
@@ -252,6 +267,9 @@
             NativeWrapper createNativeWrapper() {
                 return mNativeWrapperMock;
             }
+            IPower createIPower() {
+                return mIPowerMock;
+            }
         });
         return mService;
     }
@@ -261,6 +279,9 @@
             NativeWrapper createNativeWrapper() {
                 return new NativeWrapperFake();
             }
+            IPower createIPower() {
+                return mIPowerMock;
+            }
         });
         return mService;
     }
@@ -728,6 +749,102 @@
         verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean());
     }
 
+    @Test
+    public void testGetChannel() throws Exception {
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        // Should only call once, after caching the first call
+        ChannelConfig config = service.getBinderServiceInstance().getSessionChannel(token);
+        ChannelConfig config2 = service.getBinderServiceInstance().getSessionChannel(token);
+        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
+        assertEquals(config.readFlagBitmask, mConfig.readFlagBitmask);
+        assertEquals(config.writeFlagBitmask, mConfig.writeFlagBitmask);
+        assertEquals(config2.readFlagBitmask, mConfig.readFlagBitmask);
+        assertEquals(config2.writeFlagBitmask, mConfig.writeFlagBitmask);
+    }
+
+    @Test
+    public void testGetChannelTwice() throws Exception {
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        service.getBinderServiceInstance().getSessionChannel(token);
+        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
+        service.getBinderServiceInstance().closeSessionChannel();
+        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
+
+        clearInvocations(mIPowerMock);
+
+        service.getBinderServiceInstance().getSessionChannel(token);
+        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
+        service.getBinderServiceInstance().closeSessionChannel();
+        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
+    }
+
+    @Test
+    public void testGetChannelFails() throws Exception {
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenThrow(RemoteException.class);
+
+        assertThrows(IllegalStateException.class, () -> {
+            service.getBinderServiceInstance().getSessionChannel(token);
+        });
+    }
+
+
+    @Test
+    public void testGetChannelBadVersion() throws Exception {
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(3);
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        reset(mIPowerMock);
+        when(mIPowerMock.getInterfaceVersion()).thenReturn(3);
+        when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig);
+
+        ChannelConfig channel = service.getBinderServiceInstance().getSessionChannel(token);
+        verify(mIPowerMock, times(0)).getSessionChannel(eq(TGID), eq(UID));
+        assertNull(channel);
+    }
+
+    @Test
+    public void testCloseChannel() throws Exception {
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        service.getBinderServiceInstance().getSessionChannel(token);
+        service.getBinderServiceInstance().closeSessionChannel();
+        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
+    }
+
+    @Test
+    public void testCloseChannelFails() throws Exception {
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        service.getBinderServiceInstance().getSessionChannel(token);
+
+        doThrow(RemoteException.class).when(mIPowerMock).closeSessionChannel(anyInt(), anyInt());
+
+        assertThrows(IllegalStateException.class, () -> {
+            service.getBinderServiceInstance().closeSessionChannel();
+        });
+    }
+
+    @Test
+    public void testDoubleClose() throws Exception {
+        HintManagerService service = createService();
+        Binder token = new Binder();
+
+        service.getBinderServiceInstance().getSessionChannel(token);
+        service.getBinderServiceInstance().closeSessionChannel();
+        service.getBinderServiceInstance().closeSessionChannel();
+        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
+    }
+
     // This test checks that concurrent operations from different threads on IHintService,
     // IHintSession and UidObserver will not cause data race or deadlock. Ideally we should also
     // check the output of threads' reportActualDuration performance to detect lock starvation
@@ -935,4 +1052,40 @@
         a.reportActualWorkDuration2(WORK_DURATIONS_FIVE);
         verify(mNativeWrapperMock, never()).halReportActualWorkDuration(anyLong(), any(), any());
     }
+
+    @Test
+    public void testChannelDiesWhenTokenDies() throws Exception {
+        HintManagerService service = createService();
+
+        class DyingToken extends Binder {
+            DeathRecipient mToNotify;
+            @Override
+            public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
+                mToNotify = recipient;
+                super.linkToDeath(recipient, flags);
+            }
+
+            public void fakeDeath() {
+                mToNotify.binderDied();
+            }
+        }
+
+        DyingToken token = new DyingToken();
+
+        service.getBinderServiceInstance().getSessionChannel(token);
+        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
+        assertTrue(service.hasChannel(TGID, UID));
+
+        token.fakeDeath();
+
+        assertFalse(service.hasChannel(TGID, UID));
+        verify(mIPowerMock, times(1)).closeSessionChannel(eq(TGID), eq(UID));
+
+        clearInvocations(mIPowerMock);
+
+        token = new DyingToken();
+        service.getBinderServiceInstance().getSessionChannel(token);
+        verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID));
+        assertTrue(service.hasChannel(TGID, UID));
+    }
 }
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 5e2fe6a..2d672b8 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -5969,6 +5969,8 @@
         assertThat(captor.getValue().getNotification().flags
                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                 FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
         assertThat(captor.getValue().shouldPostSilently()).isTrue();
     }
 
@@ -8798,6 +8800,8 @@
         assertThat(captor.getValue().getNotification().flags
                 & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
                 FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+        assertThat(captor.getValue().getNotification().flags
+                & FLAG_ONLY_ALERT_ONCE).isEqualTo(FLAG_ONLY_ALERT_ONCE);
         assertThat(captor.getValue().shouldPostSilently()).isTrue();
     }
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 5fdb396..d1423fe 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -3002,39 +3002,45 @@
         assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
     }
 
-    private enum ModesApiFlag {
-        ENABLED(true, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
-        DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
+    private enum ModesFlag {
+        MODES_UI(2, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
+        MODES_API(1, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER),
+        DISABLED(0, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
 
-        private final boolean mEnabled;
+        private final int mFlagsEnabled;
         @ConfigChangeOrigin
         private final int mOriginForUserActionInSystemUi;
 
-        ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
-            this.mEnabled = enabled;
+        ModesFlag(int flagsEnabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
+            this.mFlagsEnabled = flagsEnabled;
             this.mOriginForUserActionInSystemUi = originForUserActionInSystemUi;
         }
 
-        void applyFlag(SetFlagsRule setFlagsRule) {
-            if (mEnabled) {
+        void applyFlags(SetFlagsRule setFlagsRule) {
+            if (mFlagsEnabled >= 1) {
                 setFlagsRule.enableFlags(Flags.FLAG_MODES_API);
             } else {
                 setFlagsRule.disableFlags(Flags.FLAG_MODES_API);
             }
+            if (mFlagsEnabled >= 2) {
+                setFlagsRule.enableFlags(Flags.FLAG_MODES_UI);
+            } else {
+                setFlagsRule.disableFlags(Flags.FLAG_MODES_UI);
+            }
         }
     }
 
     @Test
-    public void testZenModeEventLog_setManualZenMode(@TestParameter ModesApiFlag modesApiFlag)
+    public void testZenModeEventLog_setManualZenMode(@TestParameter ModesFlag modesFlag)
             throws IllegalArgumentException {
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
         // Turn zen mode on (to important_interruptions)
         // Need to additionally call the looper in order to finish the post-apply-config process
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
@@ -3062,7 +3068,7 @@
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo(
-                modesApiFlag == ModesApiFlag.DISABLED);
+                modesFlag == ModesFlag.DISABLED);
         assertTrue(mZenModeEventLogger.getIsUserAction(0));
         assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0));
         checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
@@ -3091,9 +3097,9 @@
     }
 
     @Test
-    public void testZenModeEventLog_automaticRules(@TestParameter ModesApiFlag modesApiFlag)
+    public void testZenModeEventLog_automaticRules(@TestParameter ModesFlag modesFlag)
             throws IllegalArgumentException {
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -3116,7 +3122,7 @@
         // Event 2: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
         mZenModeHelper.updateAutomaticZenRule(id, zenRule,
-                modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
 
         // Add a new system rule
         AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -3134,7 +3140,7 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // Event 4: "User" deletes the rule
-        mZenModeHelper.removeAutomaticZenRule(systemId, modesApiFlag.mOriginForUserActionInSystemUi,
+        mZenModeHelper.removeAutomaticZenRule(systemId, modesFlag.mOriginForUserActionInSystemUi,
                 "", Process.SYSTEM_UID);
 
         // In total, this represents 4 events
@@ -3282,22 +3288,22 @@
     }
 
     @Test
-    public void testZenModeEventLog_policyChanges(@TestParameter ModesApiFlag modesApiFlag)
+    public void testZenModeEventLog_policyChanges(@TestParameter ModesFlag modesFlag)
             throws IllegalArgumentException {
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
         // First just turn zen mode on
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID);
 
         // Now change the policy slightly; want to confirm that this'll be reflected in the logs
         ZenModeConfig newConfig = mZenModeHelper.mConfig.copy();
         newConfig.allowAlarms = true;
         newConfig.allowRepeatCallers = false;
         mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
-                modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
 
         // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
         // is off.
@@ -3308,7 +3314,7 @@
         newConfig.allowMessages = false;
         newConfig.allowRepeatCallers = true;
         mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(),
-                modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID);
 
         // Total events: we only expect ones for turning on, changing policy, and turning off
         assertEquals(3, mZenModeEventLogger.numLoggedChanges());
@@ -3341,9 +3347,9 @@
     }
 
     @Test
-    public void testZenModeEventLog_ruleCounts(@TestParameter ModesApiFlag modesApiFlag)
+    public void testZenModeEventLog_ruleCounts(@TestParameter ModesFlag modesFlag)
             throws IllegalArgumentException {
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -3447,9 +3453,9 @@
 
     @Test
     public void testZenModeEventLog_noLogWithNoConfigChange(
-            @TestParameter ModesApiFlag modesApiFlag) throws IllegalArgumentException {
+            @TestParameter ModesFlag modesFlag) throws IllegalArgumentException {
         // If evaluateZenMode is called independently of a config change, don't log.
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -3466,11 +3472,11 @@
     }
 
     @Test
-    public void testZenModeEventLog_reassignUid(@TestParameter ModesApiFlag modesApiFlag)
+    public void testZenModeEventLog_reassignUid(@TestParameter ModesFlag modesFlag)
             throws IllegalArgumentException {
         // Test that, only in specific cases, we reassign the calling UID to one associated with
         // the automatic rule owner.
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -3496,7 +3502,7 @@
                 manualRulePolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
-                modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
 
         // Turn on rule 1; call looks like it's from the system. Because setting a condition is
         // typically an automatic (non-user-initiated) action, expect the calling UID to be
@@ -3515,7 +3521,7 @@
         // from the system-provided one.
         zenRule.setEnabled(false);
         mZenModeHelper.updateAutomaticZenRule(id, zenRule,
-                modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
+                modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID);
 
         // Add a manual rule. Any manual rule changes should not get calling uids reassigned.
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP,
@@ -3573,9 +3579,9 @@
 
     @Test
     public void testZenModeEventLog_channelsBypassingChanges(
-            @TestParameter ModesApiFlag modesApiFlag) {
+            @TestParameter ModesFlag modesFlag) {
         // Verify that the right thing happens when the canBypassDnd value changes.
-        modesApiFlag.applyFlag(mSetFlagsRule);
+        modesFlag.applyFlags(mSetFlagsRule);
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
@@ -3847,8 +3853,9 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() {
+    public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDefault(
+            @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+        modesFlag.applyFlags(mSetFlagsRule);
         setupZenConfig();
 
         // When there's one automatic rule active and it doesn't specify a policy, test that the
@@ -3869,7 +3876,9 @@
 
         // inspect the consolidated policy, which should match the device default settings.
         assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy))
-                .isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+                .isEqualTo(modesFlag == ModesFlag.MODES_UI
+                        ? mZenModeHelper.getDefaultZenPolicy()
+                        : mZenModeHelper.mConfig.toZenPolicy());
     }
 
     @Test
@@ -3904,7 +3913,8 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // since this is the only active rule, the consolidated policy should match the custom
-        // policy for every field specified, and take default values for unspecified things
+        // policy for every field specified, and take default values (from device default or
+        // manual policy) for unspecified things
         assertTrue(mZenModeHelper.mConsolidatedPolicy.allowAlarms());  // custom
         assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMedia());  // custom
         assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem());  // default
@@ -3918,8 +3928,9 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() {
+    public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDefault(
+            @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+        modesFlag.applyFlags(mSetFlagsRule);
         setupZenConfig();
 
         // when there's only one automatic rule active and it has a custom policy, make sure that's
@@ -3948,11 +3959,15 @@
                 UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
 
         // since this is the only active rule, the consolidated policy should match the custom
-        // policy for every field specified, and take default values for unspecified things
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue();  // default
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue();  // default
+        // policy for every field specified, and take default values (from either device default
+        // policy or manual rule) for unspecified things
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? true : false);  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? true : false);  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue();  // custom
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? false : true);  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse();  // custom
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse();  // custom
@@ -4022,8 +4037,9 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() {
+    public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault(
+            @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+        modesFlag.applyFlags(mSetFlagsRule);
         setupZenConfig();
 
         // when there are two rules active, one inheriting the default policy and one setting its
@@ -4071,16 +4087,19 @@
         // now both rules should be on, and the consolidated policy should reflect the most
         // restrictive option of each of the two
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse();  // custom stricter
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? true : false);  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse();  // default stricter
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? false : true);  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse();  // custom stricter
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue();  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers())
                 .isFalse();  // custom stricter
         assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse();  // custom stricter
-        assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse();  // default stricter
+        assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? false : true);  // default
     }
 
     @Test
@@ -4134,8 +4153,9 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MODES_API)
-    public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll() {
+    public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll(
+            @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) {
+        modesFlag.applyFlags(mSetFlagsRule);
         setupZenConfig();
 
         // Rules with INTERRUPTION_FILTER_ALL are skipped when calculating consolidated policy.
@@ -4172,10 +4192,12 @@
                 UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
 
         // Consolidated Policy should be default + rule1.
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? true : false);  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue();  // priority rule
-        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse();  // default
+        assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo(
+                modesFlag == ModesFlag.MODES_UI ? false : true);  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue();  // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
         assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue();  // default
@@ -6251,7 +6273,7 @@
     }
 
     private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) {
-        if (!Flags.modesApi()) {
+        if (!Flags.modesUi()) {
             checkDndProtoMatchesSetupZenConfig(dndProto);
             return;
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 10eae57..1355092 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -46,7 +46,6 @@
 import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
-import static android.server.wm.ActivityManagerTestBase.isTablet;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
@@ -76,7 +75,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -124,7 +122,6 @@
 import com.android.server.wm.BackgroundActivityStartController.BalVerdict;
 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
 import com.android.server.wm.utils.MockTracker;
-import com.android.window.flags.Flags;
 
 import org.junit.After;
 import org.junit.Before;
@@ -1298,12 +1295,6 @@
      */
     @Test
     public void testDeliverIntentToTopActivityOfNonTopDisplay() {
-        // TODO(b/330152508): Remove check once legacy multi-display behaviour can coexist with
-        //  desktop windowing mode
-        // Ignore test if desktop windowing is enabled on tablets as legacy multi-display
-        // behaviour will not be respected
-        assumeFalse(Flags.enableDesktopWindowingMode() && isTablet());
-
         final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK,
                 false /* mockGetRootTask */);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
index 695faa5..39a2259 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
@@ -19,6 +19,8 @@
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
 import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
+import static com.android.window.flags.Flags.balImprovedMetrics;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -145,6 +147,16 @@
         }
 
         @Override
+        boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
+            return true;
+        }
+
+        @Override
+        boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
+            return true;
+        }
+
+        @Override
         BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
             return mCallerVerdict.orElseGet(
                     () -> super.checkBackgroundActivityStartAllowedByCaller(state));
@@ -238,7 +250,12 @@
 
         // assertions
         assertThat(verdict.getCode()).isEqualTo(BackgroundActivityStartController.BAL_BLOCK);
-        assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+        if (balImprovedMetrics()) {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+        } else {
+            assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+        }
     }
 
     // Tests for BackgroundActivityStartController.checkBackgroundActivityStart
@@ -268,7 +285,12 @@
 
         // assertions
         assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
-        assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+        if (balImprovedMetrics()) {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+        } else {
+            assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+        }
     }
 
     @Test
@@ -298,7 +320,12 @@
 
         // assertions
         assertThat(verdict).isEqualTo(callerVerdict);
-        assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+        if (balImprovedMetrics()) {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+        } else {
+            assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+        }
     }
 
     @Test
@@ -362,7 +389,13 @@
 
         // assertions
         assertThat(verdict).isEqualTo(callerVerdict);
-        assertThat(mBalAllowedLogs).containsExactly(new BalAllowedLog("", callerVerdict.getCode()));
+        if (balImprovedMetrics()) {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+        } else {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("", callerVerdict.getCode()));
+        }
     }
 
     @Test
@@ -398,7 +431,12 @@
 
         // assertions
         assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
-        assertThat(mBalAllowedLogs).isEmpty();
+        if (balImprovedMetrics()) {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+        } else {
+            assertThat(mBalAllowedLogs).isEmpty();
+        }
     }
 
     @Test
@@ -430,7 +468,12 @@
 
         // assertions
         assertThat(verdict).isEqualTo(callerVerdict);
-        assertThat(mBalAllowedLogs).isEmpty();
+        if (balImprovedMetrics()) {
+            assertThat(mBalAllowedLogs).containsExactly(
+                    new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+        } else {
+            assertThat(mBalAllowedLogs).isEmpty();
+        }
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 44d1b54..87395a1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -113,15 +113,10 @@
 import android.metrics.LogMaker;
 import android.os.Binder;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.ArraySet;
-import android.util.DisplayMetrics;
 import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
@@ -131,7 +126,6 @@
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWindowManager;
 import android.view.InsetsState;
-import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
@@ -151,7 +145,6 @@
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.wm.utils.WmDisplayCutout;
 
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -178,10 +171,6 @@
 @RunWith(WindowTestRunner.class)
 public class DisplayContentTests extends WindowTestsBase {
 
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule =
-            DeviceFlagsValueProvider.createCheckFlagsRule();
-
     @SetupWindows(addAllCommonWindows = true)
     @Test
     public void testForAllWindows() {
@@ -514,44 +503,6 @@
         assertEquals(currentConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
     }
 
-    /**
-     * Tests tapping on a root task in different display results in window gaining focus.
-     */
-    @Test
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM)
-    public void testInputEventBringsCorrectDisplayInFocus() {
-        DisplayContent dc0 = mWm.getDefaultDisplayContentLocked();
-        // Create a second display
-        final DisplayContent dc1 = createNewDisplay();
-
-        // Add root task with activity.
-        final Task rootTask0 = createTask(dc0);
-        final Task task0 = createTaskInRootTask(rootTask0, 0 /* userId */);
-        final ActivityRecord activity = createNonAttachedActivityRecord(dc0);
-        task0.addChild(activity, 0);
-        dc0.configureDisplayPolicy();
-        assertNotNull(dc0.mTapDetector);
-
-        final Task rootTask1 = createTask(dc1);
-        final Task task1 = createTaskInRootTask(rootTask1, 0 /* userId */);
-        final ActivityRecord activity1 = createNonAttachedActivityRecord(dc0);
-        task1.addChild(activity1, 0);
-        dc1.configureDisplayPolicy();
-        assertNotNull(dc1.mTapDetector);
-
-        // tap on primary display.
-        tapOnDisplay(dc0);
-        // Check focus is on primary display.
-        assertEquals(mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus,
-                dc0.findFocusedWindow());
-
-        // Tap on secondary display.
-        tapOnDisplay(dc1);
-        // Check focus is on secondary.
-        assertEquals(mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus,
-                dc1.findFocusedWindow());
-    }
-
     @Test
     public void testFocusedWindowMultipleDisplays() {
         doTestFocusedWindowMultipleDisplays(false /* perDisplayFocusEnabled */, Q);
@@ -2959,33 +2910,4 @@
             throw new RuntimeException(e);
         }
     }
-
-    private void tapOnDisplay(final DisplayContent dc) {
-        final DisplayMetrics dm = dc.getDisplayMetrics();
-        final float x = dm.widthPixels / 2;
-        final float y = dm.heightPixels / 2;
-        final long downTime = SystemClock.uptimeMillis();
-        final long eventTime = SystemClock.uptimeMillis() + 100;
-        // sending ACTION_DOWN
-        final MotionEvent downEvent = MotionEvent.obtain(
-                downTime,
-                downTime,
-                MotionEvent.ACTION_DOWN,
-                x,
-                y,
-                0 /*metaState*/);
-        downEvent.setDisplayId(dc.getDisplayId());
-        dc.mTapDetector.onPointerEvent(downEvent);
-
-        // sending ACTION_UP
-        final MotionEvent upEvent = MotionEvent.obtain(
-                downTime,
-                eventTime,
-                MotionEvent.ACTION_UP,
-                x,
-                y,
-                0 /*metaState*/);
-        upEvent.setDisplayId(dc.getDisplayId());
-        dc.mTapDetector.onPointerEvent(upEvent);
-    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index b74da1a..a60d243 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -85,6 +85,8 @@
 import android.content.pm.PackageManager.Property;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsSource;
 import android.view.InsetsState;
@@ -96,6 +98,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
+import com.android.window.flags.Flags;
 
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -1528,6 +1531,98 @@
                         mActivity.getParent().getConfiguration()), /* delta */  0.01);
     }
 
+    @Test
+    public void testIsVerticalThinLetterboxed() {
+        // Vertical thin letterbox disabled
+        doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration)
+                .getThinLetterboxHeightPx();
+        assertFalse(mController.isVerticalThinLetterboxed());
+        // Define a Task 100x100
+        final Task task = mock(Task.class);
+        doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
+        doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration)
+                .getThinLetterboxHeightPx();
+
+        // Vertical thin letterbox disabled without Task
+        doReturn(null).when(mActivity).getTask();
+        assertFalse(mController.isVerticalThinLetterboxed());
+        // Assign a Task for the Activity
+        doReturn(task).when(mActivity).getTask();
+
+        // (task.width() - act.width()) / 2  = 5 < 10
+        doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds();
+        assertTrue(mController.isVerticalThinLetterboxed());
+
+        // (task.width() - act.width()) / 2  = 10 = 10
+        doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds();
+        assertTrue(mController.isVerticalThinLetterboxed());
+
+        // (task.width() - act.width()) / 2  = 11 > 10
+        doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds();
+        assertFalse(mController.isVerticalThinLetterboxed());
+    }
+
+    @Test
+    public void testIsHorizontalThinLetterboxed() {
+        // Horizontal thin letterbox disabled
+        doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration)
+                .getThinLetterboxWidthPx();
+        assertFalse(mController.isHorizontalThinLetterboxed());
+        // Define a Task 100x100
+        final Task task = mock(Task.class);
+        doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds();
+        doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration)
+                .getThinLetterboxWidthPx();
+
+        // Vertical thin letterbox disabled without Task
+        doReturn(null).when(mActivity).getTask();
+        assertFalse(mController.isHorizontalThinLetterboxed());
+        // Assign a Task for the Activity
+        doReturn(task).when(mActivity).getTask();
+
+        // (task.height() - act.height()) / 2  = 5 < 10
+        doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds();
+        assertTrue(mController.isHorizontalThinLetterboxed());
+
+        // (task.height() - act.height()) / 2  = 10 = 10
+        doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds();
+        assertTrue(mController.isHorizontalThinLetterboxed());
+
+        // (task.height() - act.height()) / 2  = 11 > 10
+        doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds();
+        assertFalse(mController.isHorizontalThinLetterboxed());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_REACHABILITY)
+    public void testAllowReachabilityForThinLetterboxWithFlagEnabled() {
+        spyOn(mController);
+        doReturn(true).when(mController).isVerticalThinLetterboxed();
+        assertFalse(mController.allowVerticalReachabilityForThinLetterbox());
+        doReturn(true).when(mController).isHorizontalThinLetterboxed();
+        assertFalse(mController.allowHorizontalReachabilityForThinLetterbox());
+
+        doReturn(false).when(mController).isVerticalThinLetterboxed();
+        assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
+        doReturn(false).when(mController).isHorizontalThinLetterboxed();
+        assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_REACHABILITY)
+    public void testAllowReachabilityForThinLetterboxWithFlagDisabled() {
+        spyOn(mController);
+        doReturn(true).when(mController).isVerticalThinLetterboxed();
+        assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
+        doReturn(true).when(mController).isHorizontalThinLetterboxed();
+        assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+
+        doReturn(false).when(mController).isVerticalThinLetterboxed();
+        assertTrue(mController.allowVerticalReachabilityForThinLetterbox());
+        doReturn(false).when(mController).isHorizontalThinLetterboxed();
+        assertTrue(mController.allowHorizontalReachabilityForThinLetterbox());
+    }
+
     private void mockThatProperty(String propertyName, boolean value) throws Exception {
         Property property = new Property(propertyName, /* value */ value, /* packageName */ "",
                 /* className */ "");
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 8677738..6b605ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -3646,11 +3646,27 @@
     }
 
     @Test
+    public void testIsReachabilityEnabled_thisLetterbox_false() {
+        // Case when the reachability would be enabled otherwise
+        setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+        mActivity.getWindowConfiguration().setBounds(null);
+
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false);
+
+        assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled());
+        assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled());
+    }
+
+    @Test
     public void testIsHorizontalReachabilityEnabled_splitScreen_false() {
         mAtm.mDevEnableNonResizableMultiWindow = true;
         setUpDisplaySizeWithApp(2800, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
         final TestSplitOrganizer organizer =
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
 
@@ -3673,6 +3689,7 @@
         setUpDisplaySizeWithApp(1000, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
         final TestSplitOrganizer organizer =
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
 
@@ -3694,6 +3711,7 @@
         setUpDisplaySizeWithApp(1000, 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable landscape-only activity.
         prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE);
@@ -3715,6 +3733,7 @@
         setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
 
@@ -3731,6 +3750,7 @@
         setUpDisplaySizeWithApp(/* dw */ 2800, /* dh */ 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
@@ -3747,6 +3767,7 @@
         // Portrait display
         setUpDisplaySizeWithApp(1400, 1600);
         mActivity.mWmService.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // 16:9f unresizable portrait app
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
@@ -3760,6 +3781,7 @@
         // Landscape display
         setUpDisplaySizeWithApp(1600, 1500);
         mActivity.mWmService.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // 16:9f unresizable landscape app
         prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
@@ -3773,6 +3795,7 @@
         setUpDisplaySizeWithApp(2800, 1000);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable portrait-only activity.
         prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT);
@@ -3794,6 +3817,7 @@
         setUpDisplaySizeWithApp(1800, 2200);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable portrait-only activity.
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
@@ -3815,6 +3839,7 @@
         setUpDisplaySizeWithApp(2200, 1800);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+        setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true);
 
         // Unresizable landscape-only activity.
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
@@ -5011,6 +5036,14 @@
         assertFalse(activity.shouldSendCompatFakeFocus());
     }
 
+    private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) {
+        spyOn(mActivity.mLetterboxUiController);
+        doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController)
+                .allowVerticalReachabilityForThinLetterbox();
+        doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController)
+                .allowHorizontalReachabilityForThinLetterbox();
+    }
+
     private int getExpectedSplitSize(int dimensionToSplit) {
         int dividerWindowWidth =
                 mActivity.mWmService.mContext.getResources().getDimensionPixelSize(
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 83e4151..afa6698 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -487,6 +487,16 @@
         // Flush EVENT_APPEARED.
         mController.dispatchPendingEvents();
 
+        // Even if the activity is not launched in an organized TaskFragment, it is still considered
+        // as the remote activity to the organizer process. Because when the task becomes visible,
+        // the organizer process needs to be interactive (unfrozen) to receive TaskFragment events.
+        activity.setVisibleRequested(true);
+        activity.setState(ActivityRecord.State.RESUMED, "test");
+        assertTrue(organizerProc.hasVisibleActivities());
+        activity.setVisibleRequested(false);
+        activity.setState(ActivityRecord.State.STOPPED, "test");
+        assertFalse(organizerProc.hasVisibleActivities());
+
         // Make sure the activity belongs to the same app, but it is in a different pid.
         activity.info.applicationInfo.uid = uid;
         doReturn(pid + 1).when(activity).getPid();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 37de51e..4fc222b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -95,10 +95,6 @@
     }
 
     @Override
-    public void updatePointerIcon(float x, float y) throws RemoteException {
-    }
-
-    @Override
     public void dispatchWindowShown() throws RemoteException {
     }
 
@@ -128,4 +124,9 @@
     public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
             throws RemoteException {
     }
+
+    @Override
+    public void dumpWindow(ParcelFileDescriptor pfd) {
+
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 9d14290..2e93cba 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -654,7 +654,10 @@
                 }
             } else if (Intent.ACTION_USER_STARTED.equals(action)) {
                 if (userId >= 0) {
-                    mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget();
+                    if (!Flags.disableIdleCheck() || userId > 0) {
+                        // Don't check idle state for USER_SYSTEM during the boot up.
+                        mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget();
+                    }
                 }
             }
         }
@@ -2013,6 +2016,8 @@
                 + ": " + Flags.useParceledList());
         pw.println("    " + Flags.FLAG_FILTER_BASED_EVENT_QUERY_API
                 + ": " + Flags.filterBasedEventQueryApi());
+        pw.println("    " + Flags.FLAG_DISABLE_IDLE_CHECK
+                + ": " + Flags.disableIdleCheck());
 
         final int[] userIds;
         synchronized (mLock) {
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index 40537c8..6d53c27 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -16,6 +16,8 @@
 
 package com.android.server.usb;
 
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
+
 import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
 
 import android.Manifest;
@@ -43,6 +45,7 @@
 import android.os.Environment;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.service.usb.UsbProfileGroupSettingsManagerProto;
 import android.service.usb.UsbSettingsAccessoryPreferenceProto;
 import android.service.usb.UsbSettingsDevicePreferenceProto;
@@ -939,13 +942,24 @@
     }
 
     /**
-     * @return true if any application in foreground have set restrict_usb_overlay_activities as
-     * true in manifest file. The application needs to have MANAGE_USB permission.
+     * @return true if the user has not finished the setup process or if there are any
+     * foreground applications with MANAGE_USB permission and restrict_usb_overlay_activities
+     * enabled in the manifest file.
      */
     private boolean shouldRestrictOverlayActivities() {
 
         if (!Flags.allowRestrictionOfOverlayActivities()) return false;
 
+        if (Settings.Secure.getIntForUser(
+                mContext.getContentResolver(),
+                USER_SETUP_COMPLETE,
+                /* defaultValue= */ 1,
+                UserHandle.CURRENT.getIdentifier())
+                == 0) {
+            Slog.d(TAG, "restricting usb overlay activities as setup is not complete");
+            return true;
+        }
+
         List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager
                 .getRunningAppProcesses();
 
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 0fc9d6f..3c72498 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -28,14 +28,11 @@
 import android.os.SystemClock
 import android.os.test.TestLooper
 import android.platform.test.annotations.Presubmit
-import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.provider.Settings
 import android.view.View.OnKeyListener
-import android.view.Display
 import android.view.InputDevice
 import android.view.KeyEvent
-import android.view.PointerIcon
 import android.view.SurfaceHolder
 import android.view.SurfaceView
 import android.test.mock.MockContentResolver
@@ -44,7 +41,6 @@
 import com.google.common.truth.Truth.assertThat
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import org.junit.After
-import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
@@ -53,22 +49,16 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.Mockito.verifyZeroInteractions
 import org.mockito.junit.MockitoJUnit
 import org.mockito.stubbing.OngoingStubbing
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 /**
  * Tests for {@link InputManagerService}.
@@ -179,203 +169,6 @@
         localService.setDisplayViewports(viewports)
         verify(native).setDisplayViewports(any(Array<DisplayViewport>::class.java))
         verify(native).setPointerDisplayId(displayId)
-
-        val x = 42f
-        val y = 314f
-        service.onPointerDisplayIdChanged(displayId, x, y)
-        testLooper.dispatchNext()
-        verify(wmCallbacks).notifyPointerDisplayIdChanged(displayId, x, y)
-    }
-
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
-    @Test
-    fun testSetVirtualMousePointerDisplayId() {
-        // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
-        // until the native callback happens.
-        var countDownLatch = CountDownLatch(1)
-        val overrideDisplayId = 123
-        Thread {
-            assertTrue("Setting virtual pointer display should succeed",
-                localService.setVirtualMousePointerDisplayId(overrideDisplayId))
-            countDownLatch.countDown()
-        }.start()
-        assertFalse("Setting virtual pointer display should block",
-            countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
-        val x = 42f
-        val y = 314f
-        service.onPointerDisplayIdChanged(overrideDisplayId, x, y)
-        testLooper.dispatchNext()
-        verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, x, y)
-        assertTrue("Native callback unblocks calling thread",
-            countDownLatch.await(100, TimeUnit.MILLISECONDS))
-        verify(native).setPointerDisplayId(overrideDisplayId)
-
-        // Ensure that setting the same override again succeeds immediately.
-        assertTrue("Setting the same virtual mouse pointer displayId again should succeed",
-            localService.setVirtualMousePointerDisplayId(overrideDisplayId))
-
-        // Ensure that we did not query WM for the pointerDisplayId when setting the override
-        verify(wmCallbacks, never()).pointerDisplayId
-
-        // Unset the virtual mouse pointer displayId, and ensure that we query WM for the new
-        // pointer displayId and the calling thread is blocked until the native callback happens.
-        countDownLatch = CountDownLatch(1)
-        val pointerDisplayId = 42
-        `when`(wmCallbacks.pointerDisplayId).thenReturn(pointerDisplayId)
-        Thread {
-            assertTrue("Unsetting virtual mouse pointer displayId should succeed",
-                localService.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY))
-            countDownLatch.countDown()
-        }.start()
-        assertFalse("Unsetting virtual mouse pointer displayId should block",
-            countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
-        service.onPointerDisplayIdChanged(pointerDisplayId, x, y)
-        testLooper.dispatchNext()
-        verify(wmCallbacks).notifyPointerDisplayIdChanged(pointerDisplayId, x, y)
-        assertTrue("Native callback unblocks calling thread",
-            countDownLatch.await(100, TimeUnit.MILLISECONDS))
-        verify(native).setPointerDisplayId(pointerDisplayId)
-    }
-
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
-    @Test
-    fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() {
-        // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
-        // until the native callback happens.
-        val countDownLatch = CountDownLatch(1)
-        val overrideDisplayId = 123
-        Thread {
-            assertFalse("Setting virtual pointer display should be unsuccessful",
-                localService.setVirtualMousePointerDisplayId(overrideDisplayId))
-            countDownLatch.countDown()
-        }.start()
-        assertFalse("Setting virtual pointer display should block",
-            countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
-        val x = 42f
-        val y = 314f
-        // Assume the native callback updates the pointerDisplayId to the incorrect value.
-        service.onPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
-        testLooper.dispatchNext()
-        verify(wmCallbacks).notifyPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
-        assertTrue("Native callback unblocks calling thread",
-            countDownLatch.await(100, TimeUnit.MILLISECONDS))
-        verify(native).setPointerDisplayId(overrideDisplayId)
-    }
-
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
-    @Test
-    fun testSetVirtualMousePointerDisplayId_competingRequests() {
-        val firstRequestSyncLatch = CountDownLatch(1)
-        doAnswer {
-            firstRequestSyncLatch.countDown()
-        }.`when`(native).setPointerDisplayId(anyInt())
-
-        val firstRequestLatch = CountDownLatch(1)
-        val firstOverride = 123
-        Thread {
-            assertFalse("Setting virtual pointer display from thread 1 should be unsuccessful",
-                localService.setVirtualMousePointerDisplayId(firstOverride))
-            firstRequestLatch.countDown()
-        }.start()
-        assertFalse("Setting virtual pointer display should block",
-            firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
-
-        assertTrue("Wait for first thread's request should succeed",
-            firstRequestSyncLatch.await(100, TimeUnit.MILLISECONDS))
-
-        val secondRequestLatch = CountDownLatch(1)
-        val secondOverride = 42
-        Thread {
-            assertTrue("Setting virtual mouse pointer from thread 2 should be successful",
-                localService.setVirtualMousePointerDisplayId(secondOverride))
-            secondRequestLatch.countDown()
-        }.start()
-        assertFalse("Setting virtual mouse pointer should block",
-            secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
-
-        val x = 42f
-        val y = 314f
-        // Assume the native callback updates directly to the second request.
-        service.onPointerDisplayIdChanged(secondOverride, x, y)
-        testLooper.dispatchNext()
-        verify(wmCallbacks).notifyPointerDisplayIdChanged(secondOverride, x, y)
-        assertTrue("Native callback unblocks first thread",
-            firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
-        assertTrue("Native callback unblocks second thread",
-            secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
-        verify(native, times(2)).setPointerDisplayId(anyInt())
-    }
-
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
-    @Test
-    fun onDisplayRemoved_resetAllAdditionalInputProperties() {
-        setVirtualMousePointerDisplayIdAndVerify(10)
-
-        localService.setPointerIconVisible(false, 10)
-        verify(native).setPointerIconVisibility(10, false)
-        verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        localService.setMousePointerAccelerationEnabled(false, 10)
-        verify(native).setMousePointerAccelerationEnabled(10, false)
-
-        service.onDisplayRemoved(10)
-        verify(native).setPointerIconVisibility(10, true)
-        verify(native).displayRemoved(eq(10))
-        verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
-        verify(native).setMousePointerAccelerationEnabled(10, true)
-        verifyNoMoreInteractions(native)
-
-        // This call should not block because the virtual mouse pointer override was never removed.
-        localService.setVirtualMousePointerDisplayId(10)
-
-        verify(native).setPointerDisplayId(eq(10))
-        verifyNoMoreInteractions(native)
-    }
-
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
-    @Test
-    fun updateAdditionalInputPropertiesForOverrideDisplay() {
-        setVirtualMousePointerDisplayIdAndVerify(10)
-
-        localService.setPointerIconVisible(false, 10)
-        verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-        verify(native).setPointerIconVisibility(10, false)
-        localService.setMousePointerAccelerationEnabled(false, 10)
-        verify(native).setMousePointerAccelerationEnabled(10, false)
-
-        localService.setPointerIconVisible(true, 10)
-        verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
-        verify(native).setPointerIconVisibility(10, true)
-        localService.setMousePointerAccelerationEnabled(true, 10)
-        verify(native).setMousePointerAccelerationEnabled(10, true)
-
-        localService.setPointerIconVisible(false, 20)
-        verify(native).setPointerIconVisibility(20, false)
-        localService.setMousePointerAccelerationEnabled(false, 20)
-        verify(native).setMousePointerAccelerationEnabled(20, false)
-        verifyNoMoreInteractions(native)
-
-        clearInvocations(native)
-        setVirtualMousePointerDisplayIdAndVerify(20)
-
-        verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
-    }
-
-    @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
-    @Test
-    fun setAdditionalInputPropertiesBeforeOverride() {
-        localService.setPointerIconVisible(false, 10)
-        localService.setMousePointerAccelerationEnabled(false, 10)
-
-        verify(native).setPointerIconVisibility(10, false)
-        verify(native).setMousePointerAccelerationEnabled(10, false)
-        verifyNoMoreInteractions(native)
-
-        setVirtualMousePointerDisplayIdAndVerify(10)
-
-        verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
     }
 
     @Test
@@ -412,20 +205,6 @@
         verify(native, times(2)).changeKeyboardLayoutAssociation()
     }
 
-    private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) {
-        val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) }
-        thread.start()
-
-        // Allow some time for the set override call to park while waiting for the native callback.
-        Thread.sleep(100 /*millis*/)
-        verify(native).setPointerDisplayId(overrideDisplayId)
-
-        service.onPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
-        testLooper.dispatchNext()
-        verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
-        thread.join(100 /*millis*/)
-    }
-
     private fun createVirtualDisplays(count: Int): List<VirtualDisplay> {
         val displayManager: DisplayManager = context.getSystemService(
                 DisplayManager::class.java
@@ -532,9 +311,8 @@
         verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent)
     }
 
-    // TODO(b/324075859): Rename this method to addUniqueIdAssociationByPort_verifyAssociations
     @Test
-    fun addUniqueIdAssociation_verifyAssociations() {
+    fun addUniqueIdAssociationByPort_verifyAssociations() {
         // Overall goal is to have 2 displays and verify that events from the InputDevice are
         // sent only to the view that is on the associated display.
         // So, associate the InputDevice with display 1, then send and verify KeyEvents.
@@ -555,7 +333,7 @@
         val inputDevice = createInputDevice()
 
         // Associate input device with display
-        service.addUniqueIdAssociation(
+        service.addUniqueIdAssociationByPort(
                 inputDevice.name,
                 virtualDisplays[0].display.displayId.toString()
         )
@@ -579,10 +357,10 @@
         verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent)
 
         // Remove association
-        service.removeUniqueIdAssociation(inputDevice.name)
+        service.removeUniqueIdAssociationByPort(inputDevice.name)
 
         // Associate with Display 2
-        service.addUniqueIdAssociation(
+        service.addUniqueIdAssociationByPort(
                 inputDevice.name,
                 virtualDisplays[1].display.displayId.toString()
         )
diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp
index f0bea3f..2909e66 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -43,6 +43,9 @@
         "libmultiplejvmtiagentsinterferenceagent",
         "libstaticjvmtiagent",
     ],
+    libs: [
+        "android.test.mock",
+    ],
     certificate: "platform",
     platform_apis: true,
     test_suites: ["device-tests"],
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
index 4780d8a..87b26a6 100644
--- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.usbtest;
 
+import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
+
 import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -32,16 +34,20 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.PackageManager.Property;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.hardware.usb.UsbDevice;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.usb.UsbHandlerManager;
 import com.android.server.usb.UsbProfileGroupSettingsManager;
 import com.android.server.usb.UsbSettingsManager;
@@ -69,6 +75,7 @@
 public class UsbProfileGroupSettingsManagerTest {
 
     private static final String TEST_PACKAGE_NAME = "testPkg";
+
     @Mock
     private Context mContext;
     @Mock
@@ -85,43 +92,78 @@
     private UserManager mUserManager;
     @Mock
     private UsbUserSettingsManager mUsbUserSettingsManager;
-    @Mock private Property mProperty;
-    private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo;
-    private PackageInfo mPackageInfo;
-    private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+    @Mock
+    private Property mRestrictUsbOverlayActivitiesProperty;
+    @Mock
+    private UsbDevice mUsbDevice;
+
+    private MockContentResolver mContentResolver;
     private MockitoSession mStaticMockSession;
 
+    private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-
-        mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo();
-        mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME};
-        mPackageInfo = new PackageInfo();
-        mPackageInfo.packageName = TEST_PACKAGE_NAME;
-        mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
-
-        when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
-        when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class));
-        when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class)))
-                .thenReturn(mContext);
-        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
-
-        mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle,
-                mUsbSettingsManager, mUsbHandlerManager);
-
         mStaticMockSession = ExtendedMockito.mockitoSession()
                 .mockStatic(Flags.class)
                 .strictness(Strictness.WARN)
                 .startMocking();
 
-        when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo);
-        when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES),
-                eq(TEST_PACKAGE_NAME))).thenReturn(mProperty);
+        when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
         when(mUserManager.getEnabledProfiles(anyInt()))
                 .thenReturn(List.of(Mockito.mock(UserInfo.class)));
-        when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
+
+        mContentResolver = new MockContentResolver();
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+        when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
+        when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class)))
+                .thenReturn(mContext);
+
+        mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(
+                mContext, mUserHandle, mUsbSettingsManager, mUsbHandlerManager);
+
+        setupDefaultConfiguration();
+    }
+
+    /**
+     * Setups the following configuration
+     *
+     * <ul>
+     * <li>Flag is enabled
+     * <li>Device setup has completed
+     * <li>There is a foreground activity with MANAGE_USB permission
+     * <li>The foreground activity has PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES enabled
+     * </ul>
+     */
+    private void setupDefaultConfiguration() throws NameNotFoundException {
+        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+
+        Settings.Secure.putInt(mContentResolver, USER_SETUP_COMPLETE, 1);
+
+        ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo =
+                new ActivityManager.RunningAppProcessInfo();
+        mRunningAppProcessInfo.pkgList = new String[] { TEST_PACKAGE_NAME };
+        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+
+        PackageInfo mPackageInfo = new PackageInfo();
+        mPackageInfo.packageName = TEST_PACKAGE_NAME;
+        mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+        when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo);
+        when(mPackageManager.getPackagesHoldingPermissions(
+                new String[] { android.Manifest.permission.MANAGE_USB },
+                PackageManager.MATCH_SYSTEM_ONLY))
+                .thenReturn(List.of(mPackageInfo));
+
+        when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(true);
+        when(mPackageManager.getProperty(
+                eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES), eq(TEST_PACKAGE_NAME)))
+                .thenReturn(mRestrictUsbOverlayActivitiesProperty);
     }
 
     @After
@@ -130,66 +172,59 @@
     }
 
     @Test
-    public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() {
-        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+    public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
+        mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
+        verify(mUsbUserSettingsManager, times(0)).queryIntentActivities(any(Intent.class));
+    }
+
+    @Test
+    public void testDeviceAttached_noForegroundActivity_resolveActivityCalled() {
         when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>());
-        when(mPackageManager.getPackagesHoldingPermissions(
-                new String[]{android.Manifest.permission.MANAGE_USB},
-                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
-        UsbDevice device = Mockito.mock(UsbDevice.class);
-        mUsbProfileGroupSettingsManager.deviceAttached(device);
+
+        mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
         verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
     }
 
     @Test
     public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() {
-        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
-        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
         when(mPackageManager.getPackagesHoldingPermissions(
-                new String[]{android.Manifest.permission.MANAGE_USB},
-                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>());
-        UsbDevice device = Mockito.mock(UsbDevice.class);
-        mUsbProfileGroupSettingsManager.deviceAttached(device);
+                new String[] { android.Manifest.permission.MANAGE_USB },
+                PackageManager.MATCH_SYSTEM_ONLY))
+                .thenReturn(new ArrayList<>());
+
+        mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
         verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
     }
 
     @Test
-    public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
-        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
-        when(mProperty.getBoolean()).thenReturn(true);
-        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
-        when(mPackageManager.getPackagesHoldingPermissions(
-                new String[]{android.Manifest.permission.MANAGE_USB},
-                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
-        UsbDevice device = Mockito.mock(UsbDevice.class);
-        mUsbProfileGroupSettingsManager.deviceAttached(device);
-        verify(mUsbUserSettingsManager, times(0))
-                .queryIntentActivities(any(Intent.class));
-    }
+    public void testDeviceAttached_restricUsbOverlayPropertyDisabled_resolveActivityCalled() {
+        when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(false);
 
-    @Test
-    public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() {
-        when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
-        when(mProperty.getBoolean()).thenReturn(false);
-        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
-        when(mPackageManager.getPackagesHoldingPermissions(
-                new String[]{android.Manifest.permission.MANAGE_USB},
-                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
-        UsbDevice device = Mockito.mock(UsbDevice.class);
-        mUsbProfileGroupSettingsManager.deviceAttached(device);
+        mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
         verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
     }
 
     @Test
-    public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() {
+    public void testDeviceAttached_flagFalse_resolveActivityCalled() {
         when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false);
-        when(mProperty.getBoolean()).thenReturn(true);
-        when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
-        when(mPackageManager.getPackagesHoldingPermissions(
-                new String[]{android.Manifest.permission.MANAGE_USB},
-                PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
-        UsbDevice device = Mockito.mock(UsbDevice.class);
-        mUsbProfileGroupSettingsManager.deviceAttached(device);
+
+        mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
         verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
     }
+
+    @Test
+    public void
+            testDeviceAttached_setupNotCompleteAndNoBlockingActivities_resolveActivityNotCalled() {
+        when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(false);
+        Settings.Secure.putInt(mContentResolver, USER_SETUP_COMPLETE, 0);
+
+        mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice);
+
+        verify(mUsbUserSettingsManager, times(0)).queryIntentActivities(any(Intent.class));
+    }
 }
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
index e3f84c1..f126000 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
@@ -24,6 +24,7 @@
 
 import android.app.Activity;
 import android.os.Bundle;
+import android.os.Process;
 import android.util.Log;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
@@ -47,8 +48,9 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        Log.v(TAG, "Create MainActivity as user " + getUserId() + " on display "
-                + getDisplayId());
+        Log.v(TAG, "Create MainActivity as user "
+                + Process.myUserHandle().getIdentifier() + " on display "
+                + getDisplay().getDisplayId());
         setContentView(R.layout.main_activity);
         mImm = getSystemService(InputMethodManager.class);
         mEditor = requireViewById(R.id.edit_text);
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 960b57c..580efe1 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -70,6 +70,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.Uri;
+import android.net.vcn.Flags;
 import android.net.vcn.IVcnStatusCallback;
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
 import android.net.vcn.VcnConfig;
@@ -82,7 +83,9 @@
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.os.test.TestLooper;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
@@ -101,6 +104,7 @@
 import com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -118,6 +122,8 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class VcnManagementServiceTest {
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final String CONTEXT_ATTRIBUTION_TAG = "VCN";
     private static final String TEST_PACKAGE_NAME =
             VcnManagementServiceTest.class.getPackage().getName();
@@ -129,7 +135,12 @@
     private static final ParcelUuid TEST_UUID_3 = new ParcelUuid(new UUID(2, 2));
     private static final VcnConfig TEST_VCN_CONFIG;
     private static final VcnConfig TEST_VCN_CONFIG_PKG_2;
-    private static final int TEST_UID = Process.FIRST_APPLICATION_UID;
+
+    private static final int TEST_UID = 1010000; // A non-system user
+    private static final UserHandle TEST_USER_HANDLE = UserHandle.getUserHandleForUid(TEST_UID);
+    private static final UserHandle TEST_USER_HANDLE_OTHER =
+            UserHandle.of(TEST_USER_HANDLE.getIdentifier() + 1);
+
     private static final String TEST_IFACE_NAME = "TEST_IFACE";
     private static final String TEST_IFACE_NAME_2 = "TEST_IFACE2";
     private static final LinkProperties TEST_LP_1 = new LinkProperties();
@@ -187,6 +198,7 @@
     private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
     private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
     private final AppOpsManager mAppOpsMgr = mock(AppOpsManager.class);
+    private final UserManager mUserManager = mock(UserManager.class);
     private final VcnContext mVcnContext = mock(VcnContext.class);
     private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper =
             mock(PersistableBundleUtils.LockingReadWriteHelper.class);
@@ -218,6 +230,9 @@
                 Context.TELEPHONY_SUBSCRIPTION_SERVICE,
                 SubscriptionManager.class);
         setupSystemService(mMockContext, mAppOpsMgr, Context.APP_OPS_SERVICE, AppOpsManager.class);
+        setupSystemService(mMockContext, mUserManager, Context.USER_SERVICE, UserManager.class);
+
+        doReturn(TEST_USER_HANDLE).when(mUserManager).getMainUser();
 
         doReturn(TEST_PACKAGE_NAME).when(mMockContext).getOpPackageName();
 
@@ -267,6 +282,8 @@
 
     @Before
     public void setUp() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_MAIN_USER);
+
         doNothing()
                 .when(mMockContext)
                 .enforceCallingOrSelfPermission(
@@ -717,10 +734,8 @@
     }
 
     @Test
-    public void testSetVcnConfigRequiresSystemUser() throws Exception {
-        doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
-                .when(mMockDeps)
-                .getBinderCallingUid();
+    public void testSetVcnConfigRequiresMainUser() throws Exception {
+        doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser();
 
         try {
             mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
@@ -832,10 +847,8 @@
     }
 
     @Test
-    public void testClearVcnConfigRequiresSystemUser() throws Exception {
-        doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
-                .when(mMockDeps)
-                .getBinderCallingUid();
+    public void testClearVcnConfigRequiresMainUser() throws Exception {
+        doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser();
 
         try {
             mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1, TEST_PACKAGE_NAME);
@@ -921,10 +934,8 @@
     }
 
     @Test
-    public void testGetConfiguredSubscriptionGroupsRequiresSystemUser() throws Exception {
-        doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
-                .when(mMockDeps)
-                .getBinderCallingUid();
+    public void testGetConfiguredSubscriptionGroupsRequiresMainUser() throws Exception {
+        doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser();
 
         try {
             mVcnMgmtSvc.getConfiguredSubscriptionGroups(TEST_PACKAGE_NAME);
diff --git a/tools/hoststubgen/TEST_MAPPING b/tools/hoststubgen/TEST_MAPPING
index f6885e1..856e6ee 100644
--- a/tools/hoststubgen/TEST_MAPPING
+++ b/tools/hoststubgen/TEST_MAPPING
@@ -1,63 +1,7 @@
-// Keep the following two TEST_MAPPINGs in sync:
-// frameworks/base/ravenwood/TEST_MAPPING
-// frameworks/base/tools/hoststubgen/TEST_MAPPING
 {
-  "presubmit": [
-    { "name": "tiny-framework-dump-test" },
-    { "name": "hoststubgentest" },
-    { "name": "hoststubgen-invoke-test" },
+  "imports": [
     {
-      "name": "RavenwoodMockitoTest_device"
-    },
-    {
-      "name": "RavenwoodBivalentTest_device"
-    },
-    // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
-    {
-      "name": "SystemUIGoogleTests",
-      "options": [
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    }
-  ],
-  "presubmit-large": [
-    {
-      "name": "SystemUITests",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
-    }
-  ],
-  "ravenwood-presubmit": [
-    {
-      "name": "RavenwoodMinimumTest",
-      "host": true
-    },
-    {
-      "name": "RavenwoodMockitoTest",
-      "host": true
-    },
-    {
-      "name": "CtsUtilTestCasesRavenwood",
-      "host": true
-    },
-    {
-      "name": "RavenwoodCoreTest",
-      "host": true
-    },
-    {
-      "name": "RavenwoodBivalentTest",
-      "host": true
+      "path": "frameworks/base/ravenwood"
     }
   ]
 }
diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig
index 3c734bc..c5bc039 100644
--- a/wifi/wifi.aconfig
+++ b/wifi/wifi.aconfig
@@ -9,3 +9,11 @@
     bug: "313038031"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "network_provider_battery_charging_status"
+    is_exported: true
+    namespace: "wifi"
+    description: "Control the API that allows setting / reading the NetworkProviderInfo's battery charging status"
+    bug: "305067231"
+}