Merge "Force path parts in intent filter with deeplinks to have leading slash."
diff --git a/Android.bp b/Android.bp
index aa65486..0315c12 100644
--- a/Android.bp
+++ b/Android.bp
@@ -150,6 +150,9 @@
     visibility: [
         // DO NOT ADD ANY MORE ENTRIES TO THIS LIST
         "//external/robolectric-shadows:__subpackages__",
+        //This will eventually replace the item above, and serves the
+        //same purpose.
+        "//external/robolectric:__subpackages__",
         "//frameworks/layoutlib:__subpackages__",
     ],
 }
@@ -384,6 +387,7 @@
         "av-types-aidl-java",
         "tv_tuner_resource_manager_aidl_interface-java",
         "soundtrigger_middleware-aidl-java",
+        "modules-utils-binary-xml",
         "modules-utils-build",
         "modules-utils-preconditions",
         "modules-utils-statemachine",
diff --git a/apct-tests/perftests/core/src/android/util/XmlPerfTest.java b/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
index e05bd2a..b83657b 100644
--- a/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
+++ b/apct-tests/perftests/core/src/android/util/XmlPerfTest.java
@@ -28,6 +28,8 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.HexDump;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java b/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java
index 76656bd..a31184c 100644
--- a/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java
+++ b/apct-tests/perftests/core/src/com/android/internal/util/FastDataPerfTest.java
@@ -22,6 +22,9 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.FastDataInput;
+import com.android.modules.utils.FastDataOutput;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -68,7 +71,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             os.reset();
-            final FastDataOutput out = FastDataOutput.obtainUsing4ByteSequences(os);
+            final FastDataOutput out = ArtFastDataOutput.obtain(os);
             try {
                 doWrite(out);
                 out.flush();
@@ -84,7 +87,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             os.reset();
-            final FastDataOutput out = FastDataOutput.obtainUsing3ByteSequences(os);
+            final FastDataOutput out = FastDataOutput.obtain(os);
             try {
                 doWrite(out);
                 out.flush();
@@ -116,7 +119,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             is.reset();
-            final FastDataInput in = FastDataInput.obtainUsing4ByteSequences(is);
+            final FastDataInput in = ArtFastDataInput.obtain(is);
             try {
                 doRead(in);
             } finally {
@@ -131,7 +134,7 @@
         final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
         while (state.keepRunning()) {
             is.reset();
-            final FastDataInput in = FastDataInput.obtainUsing3ByteSequences(is);
+            final FastDataInput in = FastDataInput.obtain(is);
             try {
                 doRead(in);
             } finally {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index ab0ac5a..4c849fe 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -32,6 +32,7 @@
 import android.annotation.RequiresPermission;
 import android.compat.Compatibility;
 import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
@@ -97,6 +98,15 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     public static final long THROW_ON_INVALID_PRIORITY_VALUE = 140852299L;
 
+    /**
+     * Require that estimated network bytes are nonnegative.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    public static final long REJECT_NEGATIVE_NETWORK_ESTIMATES = 253665015L;
+
     /** @hide */
     @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
             NETWORK_TYPE_NONE,
@@ -1890,11 +1900,13 @@
          * @return The job object to hand to the JobScheduler. This object is immutable.
          */
         public JobInfo build() {
-            return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS));
+            return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS),
+                    Compatibility.isChangeEnabled(REJECT_NEGATIVE_NETWORK_ESTIMATES));
         }
 
         /** @hide */
-        public JobInfo build(boolean disallowPrefetchDeadlines) {
+        public JobInfo build(boolean disallowPrefetchDeadlines,
+                boolean rejectNegativeNetworkEstimates) {
             // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy
             // check that would ideally be phased out instead.
             if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
@@ -1903,7 +1915,7 @@
                         " setRequiresDeviceIdle is an error.");
             }
             JobInfo jobInfo = new JobInfo(this);
-            jobInfo.enforceValidity(disallowPrefetchDeadlines);
+            jobInfo.enforceValidity(disallowPrefetchDeadlines, rejectNegativeNetworkEstimates);
             return jobInfo;
         }
 
@@ -1921,13 +1933,24 @@
     /**
      * @hide
      */
-    public final void enforceValidity(boolean disallowPrefetchDeadlines) {
+    public final void enforceValidity(boolean disallowPrefetchDeadlines,
+            boolean rejectNegativeNetworkEstimates) {
         // Check that network estimates require network type and are reasonable values.
         if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0)
                 && networkRequest == null) {
             throw new IllegalArgumentException(
                     "Can't provide estimated network usage without requiring a network");
         }
+        if (networkRequest != null && rejectNegativeNetworkEstimates) {
+            if (networkUploadBytes != NETWORK_BYTES_UNKNOWN && networkUploadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network upload bytes: " + networkUploadBytes);
+            }
+            if (networkDownloadBytes != NETWORK_BYTES_UNKNOWN && networkDownloadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network download bytes: " + networkDownloadBytes);
+            }
+        }
         final long estimatedTransfer;
         if (networkUploadBytes == NETWORK_BYTES_UNKNOWN) {
             estimatedTransfer = networkDownloadBytes;
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
index 372f9fa..32945e0 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java
@@ -20,6 +20,7 @@
 
 import android.annotation.BytesLong;
 import android.annotation.Nullable;
+import android.compat.Compatibility;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
 import android.os.Build;
@@ -88,25 +89,11 @@
      */
     public JobWorkItem(@Nullable Intent intent, @BytesLong long downloadBytes,
             @BytesLong long uploadBytes, @BytesLong long minimumChunkBytes) {
-        if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && minimumChunkBytes <= 0) {
-            throw new IllegalArgumentException("Minimum chunk size must be positive");
-        }
-        final long estimatedTransfer;
-        if (uploadBytes == NETWORK_BYTES_UNKNOWN) {
-            estimatedTransfer = downloadBytes;
-        } else {
-            estimatedTransfer = uploadBytes
-                    + (downloadBytes == NETWORK_BYTES_UNKNOWN ? 0 : downloadBytes);
-        }
-        if (minimumChunkBytes != NETWORK_BYTES_UNKNOWN && estimatedTransfer != NETWORK_BYTES_UNKNOWN
-                && minimumChunkBytes > estimatedTransfer) {
-            throw new IllegalArgumentException(
-                    "Minimum chunk size can't be greater than estimated network usage");
-        }
         mIntent = intent;
         mNetworkDownloadBytes = downloadBytes;
         mNetworkUploadBytes = uploadBytes;
         mMinimumChunkBytes = minimumChunkBytes;
+        enforceValidity(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES));
     }
 
     /**
@@ -222,7 +209,17 @@
     /**
      * @hide
      */
-    public void enforceValidity() {
+    public void enforceValidity(boolean rejectNegativeNetworkEstimates) {
+        if (rejectNegativeNetworkEstimates) {
+            if (mNetworkUploadBytes != NETWORK_BYTES_UNKNOWN && mNetworkUploadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network upload bytes: " + mNetworkUploadBytes);
+            }
+            if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN && mNetworkDownloadBytes < 0) {
+                throw new IllegalArgumentException(
+                        "Invalid network download bytes: " + mNetworkDownloadBytes);
+            }
+        }
         final long estimatedTransfer;
         if (mNetworkUploadBytes == NETWORK_BYTES_UNKNOWN) {
             estimatedTransfer = mNetworkDownloadBytes;
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 bdd1fc54..d28ebde 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -40,6 +40,8 @@
 import android.app.job.JobWorkItem;
 import android.app.usage.UsageStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -58,6 +60,7 @@
 import android.os.BatteryManagerInternal;
 import android.os.BatteryStatsInternal;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.LimitExceededException;
 import android.os.Looper;
@@ -166,6 +169,14 @@
     /** The number of the most recently completed jobs to keep track of for debugging purposes. */
     private static final int NUM_COMPLETED_JOB_HISTORY = 20;
 
+    /**
+     * Require the hosting job to specify a network constraint if the included
+     * {@link android.app.job.JobWorkItem} indicates network usage.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L;
+
     @VisibleForTesting
     public static Clock sSystemClock = Clock.systemUTC();
 
@@ -3147,10 +3158,17 @@
             return canPersist;
         }
 
-        private void validateJobFlags(JobInfo job, int callingUid) {
+        private void validateJob(JobInfo job, int callingUid) {
+            validateJob(job, callingUid, null);
+        }
+
+        private void validateJob(JobInfo job, int callingUid, @Nullable JobWorkItem jobWorkItem) {
+            final boolean rejectNegativeNetworkEstimates = CompatChanges.isChangeEnabled(
+                            JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES, callingUid);
             job.enforceValidity(
                     CompatChanges.isChangeEnabled(
-                            JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid));
+                            JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid),
+                    rejectNegativeNetworkEstimates);
             if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
@@ -3164,6 +3182,26 @@
                             + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
                 }
             }
+            if (jobWorkItem != null) {
+                jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates);
+                if (jobWorkItem.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN
+                        || jobWorkItem.getEstimatedNetworkUploadBytes()
+                        != JobInfo.NETWORK_BYTES_UNKNOWN
+                        || jobWorkItem.getMinimumNetworkChunkBytes()
+                        != JobInfo.NETWORK_BYTES_UNKNOWN) {
+                    if (job.getRequiredNetwork() == null) {
+                        final String errorMsg = "JobWorkItem implies network usage"
+                                + " but job doesn't specify a network constraint";
+                        if (CompatChanges.isChangeEnabled(
+                                REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS,
+                                callingUid)) {
+                            throw new IllegalArgumentException(errorMsg);
+                        } else {
+                            Slog.e(TAG, errorMsg);
+                        }
+                    }
+                }
+            }
         }
 
         // IJobScheduler implementation
@@ -3184,7 +3222,7 @@
                 }
             }
 
-            validateJobFlags(job, uid);
+            validateJob(job, uid);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -3212,8 +3250,7 @@
                 throw new NullPointerException("work is null");
             }
 
-            work.enforceValidity();
-            validateJobFlags(job, uid);
+            validateJob(job, uid, work);
 
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -3244,7 +3281,7 @@
                         + " not permitted to schedule jobs for other apps");
             }
 
-            validateJobFlags(job, callerUid);
+            validateJob(job, callerUid);
 
             final long ident = Binder.clearCallingIdentity();
             try {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index f731b8d..22b0968 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -41,13 +41,13 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SystemConfigFileCommitEventLogger;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.BitUtils;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
 import com.android.server.job.controllers.JobStatus;
@@ -1024,7 +1024,8 @@
                 // have a deadline. If a job is rescheduled (via jobFinished(true) or onStopJob()'s
                 // return value), the deadline is dropped. Periodic jobs require all constraints
                 // to be met, so there's no issue with their deadlines.
-                builtJob = jobBuilder.build(false);
+                // The same logic applies for other target SDK-based validation checks.
+                builtJob = jobBuilder.build(false, false);
             } catch (Exception e) {
                 Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e);
                 return null;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 669234b..de602a8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -612,9 +612,9 @@
             requestBuilder.setUids(
                     Collections.singleton(new Range<Integer>(this.sourceUid, this.sourceUid)));
             builder.setRequiredNetwork(requestBuilder.build());
-            // Don't perform prefetch-deadline check at this point. We've already passed the
+            // Don't perform validation checks at this point since we've already passed the
             // initial validation check.
-            job = builder.build(false);
+            job = builder.build(false, false);
         }
 
         updateMediaBackupExemptionStatus();
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 27d00b7..ee448b5 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -33,12 +33,12 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseArrayMap;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/api/Android.bp b/api/Android.bp
index 9306671..a3e64a5 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -98,6 +98,7 @@
         "framework-configinfrastructure",
         "framework-connectivity",
         "framework-connectivity-t",
+        "framework-devicelock",
         "framework-federatedcompute",
         "framework-graphics",
         "framework-healthconnect",
diff --git a/boot/Android.bp b/boot/Android.bp
index 9fdb9bc..7839918 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -72,6 +72,10 @@
             module: "com.android.conscrypt-bootclasspath-fragment",
         },
         {
+            apex: "com.android.devicelock",
+            module: "com.android.devicelock-bootclasspath-fragment",
+        },
+        {
             apex: "com.android.federatedcompute",
             module: "com.android.federatedcompute-bootclasspath-fragment",
         },
diff --git a/core/api/current.txt b/core/api/current.txt
index cc14512..18d8fc7 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -34,6 +34,7 @@
     field public static final String BIND_COMPANION_DEVICE_SERVICE = "android.permission.BIND_COMPANION_DEVICE_SERVICE";
     field public static final String BIND_CONDITION_PROVIDER_SERVICE = "android.permission.BIND_CONDITION_PROVIDER_SERVICE";
     field public static final String BIND_CONTROLS = "android.permission.BIND_CONTROLS";
+    field public static final String BIND_CREDENTIAL_PROVIDER_SERVICE = "android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE";
     field public static final String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN";
     field public static final String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE";
     field public static final String BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE";
@@ -106,6 +107,7 @@
     field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
     field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
     field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
+    field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE";
     field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS";
     field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE";
     field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA";
@@ -1490,6 +1492,7 @@
     field public static final int targetCellWidth = 16844340; // 0x1010634
     field public static final int targetClass = 16842799; // 0x101002f
     field @Deprecated public static final int targetDescriptions = 16843680; // 0x10103a0
+    field public static final int targetDisplayCategory;
     field public static final int targetId = 16843740; // 0x10103dc
     field public static final int targetName = 16843853; // 0x101044d
     field public static final int targetPackage = 16842785; // 0x1010021
@@ -9844,6 +9847,7 @@
     field public static final int CONTEXT_RESTRICTED = 4; // 0x4
     field public static final String CREDENTIAL_SERVICE = "credential";
     field public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
+    field public static final String DEVICE_LOCK_SERVICE = "device_lock";
     field public static final String DEVICE_POLICY_SERVICE = "device_policy";
     field public static final String DISPLAY_HASH_SERVICE = "display_hash";
     field public static final String DISPLAY_SERVICE = "display";
@@ -11135,6 +11139,7 @@
     field public int screenOrientation;
     field public int softInputMode;
     field public String targetActivity;
+    field @Nullable public String targetDisplayCategory;
     field public String taskAffinity;
     field public int theme;
     field public int uiOptions;
@@ -11990,6 +11995,7 @@
     field public static final String FEATURE_CONTROLS = "android.software.controls";
     field public static final String FEATURE_CREDENTIALS = "android.software.credentials";
     field public static final String FEATURE_DEVICE_ADMIN = "android.software.device_admin";
+    field public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock";
     field public static final String FEATURE_EMBEDDED = "android.hardware.type.embedded";
     field public static final String FEATURE_ETHERNET = "android.hardware.ethernet";
     field public static final String FEATURE_EXPANDED_PICTURE_IN_PICTURE = "android.software.expanded_picture_in_picture";
@@ -17626,6 +17632,7 @@
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> REPROCESS_MAX_CAPTURE_STALL;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> REQUEST_AVAILABLE_CAPABILITIES;
+    field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.ColorSpaceProfiles> REQUEST_AVAILABLE_COLOR_SPACE_PROFILES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.DynamicRangeProfiles> REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> REQUEST_MAX_NUM_INPUT_STREAMS;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> REQUEST_MAX_NUM_OUTPUT_PROC;
@@ -17989,6 +17996,7 @@
     field public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4; // 0x4
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE = 0; // 0x0
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE = 6; // 0x6
+    field public static final int REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES = 20; // 0x14
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO = 9; // 0x9
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT = 8; // 0x8
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT = 18; // 0x12
@@ -18340,6 +18348,15 @@
     method @NonNull public android.util.Range<java.lang.Float> getZoomRatioRange();
   }
 
+  public final class ColorSpaceProfiles {
+    ctor public ColorSpaceProfiles(@NonNull long[]);
+    method @NonNull public java.util.Set<android.graphics.ColorSpace.Named> getSupportedColorSpaces(int);
+    method @NonNull public java.util.Set<android.graphics.ColorSpace.Named> getSupportedColorSpacesForDynamicRange(int, long);
+    method @NonNull public java.util.Set<java.lang.Long> getSupportedDynamicRangeProfiles(@NonNull android.graphics.ColorSpace.Named, int);
+    method @NonNull public java.util.Set<java.lang.Integer> getSupportedImageFormatsForColorSpace(@NonNull android.graphics.ColorSpace.Named);
+    field public static final int UNSPECIFIED = -1; // 0xffffffff
+  }
+
   public final class ColorSpaceTransform {
     ctor public ColorSpaceTransform(android.util.Rational[]);
     ctor public ColorSpaceTransform(int[]);
@@ -18571,13 +18588,16 @@
 
   public final class SessionConfiguration implements android.os.Parcelable {
     ctor public SessionConfiguration(int, @NonNull java.util.List<android.hardware.camera2.params.OutputConfiguration>, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback);
+    method public void clearColorSpace();
     method public int describeContents();
+    method @Nullable public android.graphics.ColorSpace getColorSpace();
     method public java.util.concurrent.Executor getExecutor();
     method public android.hardware.camera2.params.InputConfiguration getInputConfiguration();
     method public java.util.List<android.hardware.camera2.params.OutputConfiguration> getOutputConfigurations();
     method public android.hardware.camera2.CaptureRequest getSessionParameters();
     method public int getSessionType();
     method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback();
+    method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
     method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration);
     method public void setSessionParameters(android.hardware.camera2.CaptureRequest);
     method public void writeToParcel(android.os.Parcel, int);
@@ -39319,6 +39339,7 @@
     method public final void setNotificationsShown(String[]);
     method public final void snoozeNotification(String, long);
     method public final void updateNotificationChannel(@NonNull String, @NonNull android.os.UserHandle, @NonNull android.app.NotificationChannel);
+    field public static final String ACTION_SETTINGS_HOME = "android.service.notification.action.SETTINGS_HOME";
     field public static final int FLAG_FILTER_TYPE_ALERTING = 2; // 0x2
     field public static final int FLAG_FILTER_TYPE_CONVERSATIONS = 1; // 0x1
     field public static final int FLAG_FILTER_TYPE_ONGOING = 8; // 0x8
@@ -39326,7 +39347,6 @@
     field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
     field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
     field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
-    field public static final String INTENT_CATEGORY_SETTINGS_HOME = "android.service.notification.category.SETTINGS_HOME";
     field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4
     field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1
     field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
@@ -41726,10 +41746,12 @@
     field public static final String KEY_OPPORTUNISTIC_NETWORK_PING_PONG_TIME_LONG = "opportunistic_network_ping_pong_time_long";
     field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool";
     field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
+    field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = "premium_capability_maximum_notification_count_int_array";
     field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long";
     field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING = "premium_capability_purchase_url_string";
+    field public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL = "premium_capability_supported_on_lte_bool";
     field public static final String KEY_PREVENT_CLIR_ACTIVATION_AND_DEACTIVATION_CODE_BOOL = "prevent_clir_activation_and_deactivation_code_bool";
     field public static final String KEY_RADIO_RESTART_FAILURE_CAUSES_INT_ARRAY = "radio_restart_failure_causes_int_array";
     field public static final String KEY_RCS_CONFIG_SERVER_URL_STRING = "rcs_config_server_url_string";
@@ -41793,6 +41815,8 @@
     field public static final String KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL = "voicemail_notification_persistent_bool";
     field public static final String KEY_VOICE_PRIVACY_DISABLE_UI_BOOL = "voice_privacy_disable_ui_bool";
     field public static final String KEY_VOLTE_REPLACEMENT_RAT_INT = "volte_replacement_rat_int";
+    field public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool";
+    field public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool";
     field public static final String KEY_VT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_RTT_CALL_BOOL = "vt_upgrade_supported_for_downgraded_rtt_call";
     field public static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = "vvm_cellular_data_required_bool";
     field public static final String KEY_VVM_CLIENT_PREFIX_STRING = "vvm_client_prefix_string";
@@ -44006,7 +44030,7 @@
     field public static final int PHONE_TYPE_GSM = 1; // 0x1
     field public static final int PHONE_TYPE_NONE = 0; // 0x0
     field public static final int PHONE_TYPE_SIP = 3; // 0x3
-    field public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1; // 0x1
+    field public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY = 34; // 0x22
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4; // 0x4
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED = 3; // 0x3
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED = 7; // 0x7
@@ -44014,12 +44038,13 @@
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10; // 0xa
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13; // 0xd
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; // 0xe
+    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT = 9; // 0x9
     field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED = 6; // 0x6
-    field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5; // 0x5
     field public static final int SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION = 2; // 0x2
     field public static final int SET_OPPORTUNISTIC_SUB_NO_OPPORTUNISTIC_SUB_AVAILABLE = 3; // 0x3
     field public static final int SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION = 4; // 0x4
@@ -45401,7 +45426,7 @@
     method public final int getParagraphLeft(int);
     method public final int getParagraphRight(int);
     method public float getPrimaryHorizontal(int);
-    method @Nullable public android.util.Range<java.lang.Integer> getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
+    method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
     method public float getSecondaryHorizontal(int);
     method public void getSelectionPath(int, int, android.graphics.Path);
     method public final float getSpacingAdd();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c170f74..7a22e37 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -145,6 +145,7 @@
     field public static final String INTERACT_ACROSS_USERS_FULL = "android.permission.INTERACT_ACROSS_USERS_FULL";
     field public static final String INTERNAL_SYSTEM_WINDOW = "android.permission.INTERNAL_SYSTEM_WINDOW";
     field public static final String INVOKE_CARRIER_SETUP = "android.permission.INVOKE_CARRIER_SETUP";
+    field public static final String KILL_ALL_BACKGROUND_PROCESSES = "android.permission.KILL_ALL_BACKGROUND_PROCESSES";
     field public static final String KILL_UID = "android.permission.KILL_UID";
     field public static final String LAUNCH_DEVICE_MANAGER_SETUP = "android.permission.LAUNCH_DEVICE_MANAGER_SETUP";
     field public static final String LOCAL_MAC_ADDRESS = "android.permission.LOCAL_MAC_ADDRESS";
@@ -189,6 +190,7 @@
     field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER";
     field public static final String MANAGE_SPEECH_RECOGNITION = "android.permission.MANAGE_SPEECH_RECOGNITION";
     field public static final String MANAGE_SUBSCRIPTION_PLANS = "android.permission.MANAGE_SUBSCRIPTION_PLANS";
+    field public static final String MANAGE_SUBSCRIPTION_USER_ASSOCIATION = "android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION";
     field public static final String MANAGE_TEST_NETWORKS = "android.permission.MANAGE_TEST_NETWORKS";
     field public static final String MANAGE_TIME_AND_ZONE_DETECTION = "android.permission.MANAGE_TIME_AND_ZONE_DETECTION";
     field public static final String MANAGE_UI_TRANSLATION = "android.permission.MANAGE_UI_TRANSLATION";
@@ -585,6 +587,7 @@
     field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video";
     field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio";
     field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast";
+    field public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = "android:receive_explicit_user_interaction_audio";
     field public static final String OPSTR_REQUEST_DELETE_PACKAGES = "android:request_delete_packages";
     field public static final String OPSTR_REQUEST_INSTALL_PACKAGES = "android:request_install_packages";
     field public static final String OPSTR_RUN_ANY_IN_BACKGROUND = "android:run_any_in_background";
@@ -2502,11 +2505,49 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.time.ExternalTimeSuggestion> CREATOR;
   }
 
+  public final class TimeCapabilities implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getConfigureAutoDetectionEnabledCapability();
+    method public int getSetManualTimeCapability();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilities> CREATOR;
+  }
+
+  public final class TimeCapabilitiesAndConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.app.time.TimeCapabilities getCapabilities();
+    method @NonNull public android.app.time.TimeConfiguration getConfiguration();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeCapabilitiesAndConfig> CREATOR;
+  }
+
+  public final class TimeConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method public boolean isAutoDetectionEnabled();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeConfiguration> CREATOR;
+  }
+
+  public static final class TimeConfiguration.Builder {
+    ctor public TimeConfiguration.Builder();
+    ctor public TimeConfiguration.Builder(@NonNull android.app.time.TimeConfiguration);
+    method @NonNull public android.app.time.TimeConfiguration build();
+    method @NonNull public android.app.time.TimeConfiguration.Builder setAutoDetectionEnabled(boolean);
+  }
+
   public final class TimeManager {
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void addTimeZoneDetectorListener(@NonNull java.util.concurrent.Executor, @NonNull android.app.time.TimeManager.TimeZoneDetectorListener);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTime(@NonNull android.app.time.UnixEpochTime);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean confirmTimeZone(@NonNull String);
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeState getTimeState();
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneCapabilitiesAndConfig getTimeZoneCapabilitiesAndConfig();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public android.app.time.TimeZoneState getTimeZoneState();
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public void removeTimeZoneDetectorListener(@NonNull android.app.time.TimeManager.TimeZoneDetectorListener);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTime(@NonNull android.app.time.UnixEpochTime);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean setManualTimeZone(@NonNull String);
     method @RequiresPermission(android.Manifest.permission.SUGGEST_EXTERNAL_TIME) public void suggestExternalTime(@NonNull android.app.time.ExternalTimeSuggestion);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeConfiguration(@NonNull android.app.time.TimeConfiguration);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION) public boolean updateTimeZoneConfiguration(@NonNull android.app.time.TimeZoneConfiguration);
   }
 
@@ -2514,10 +2555,19 @@
     method public void onChange();
   }
 
+  public final class TimeState implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.app.time.UnixEpochTime getUnixEpochTime();
+    method public boolean getUserShouldConfirmTime();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeState> CREATOR;
+  }
+
   public final class TimeZoneCapabilities implements android.os.Parcelable {
     method public int describeContents();
     method public int getConfigureAutoDetectionEnabledCapability();
     method public int getConfigureGeoDetectionEnabledCapability();
+    method public int getSetManualTimeZoneCapability();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneCapabilities> CREATOR;
   }
@@ -2546,6 +2596,24 @@
     method @NonNull public android.app.time.TimeZoneConfiguration.Builder setGeoDetectionEnabled(boolean);
   }
 
+  public final class TimeZoneState implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public String getId();
+    method public boolean getUserShouldConfirmId();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.TimeZoneState> CREATOR;
+  }
+
+  public final class UnixEpochTime implements android.os.Parcelable {
+    ctor public UnixEpochTime(long, long);
+    method @NonNull public android.app.time.UnixEpochTime at(long);
+    method public int describeContents();
+    method public long getElapsedRealtimeMillis();
+    method public long getUnixEpochTimeMillis();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.time.UnixEpochTime> CREATOR;
+  }
+
 }
 
 package android.app.usage {
@@ -13324,6 +13392,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList();
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int);
     method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int);
+    method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int);
     method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int);
     method public void requestEmbeddedSubscriptionInfoListRefresh();
     method public void requestEmbeddedSubscriptionInfoListRefresh(int);
@@ -13334,6 +13403,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle);
     field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
     field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI;
     field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI;
@@ -13665,6 +13735,7 @@
     field public static final int INVALID_EMERGENCY_NUMBER_DB_VERSION = -1; // 0xffffffff
     field public static final int KEY_TYPE_EPDG = 1; // 0x1
     field public static final int KEY_TYPE_WLAN = 2; // 0x2
+    field public static final int MOBILE_DATA_POLICY_AUTO_DATA_SWITCH = 3; // 0x3
     field public static final int MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL = 1; // 0x1
     field public static final int MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED = 2; // 0x2
     field public static final int NR_DUAL_CONNECTIVITY_DISABLE = 2; // 0x2
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 186e5be..ef74a3e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1170,6 +1170,20 @@
 
 }
 
+package android.hardware.camera2.params {
+
+  public final class ColorSpaceProfiles {
+    method @NonNull public java.util.Map<android.graphics.ColorSpace.Named,java.util.Map<java.lang.Integer,java.util.Set<java.lang.Long>>> getProfileMap();
+  }
+
+  public final class OutputConfiguration implements android.os.Parcelable {
+    method public void clearColorSpace();
+    method @Nullable public android.graphics.ColorSpace getColorSpace();
+    method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named);
+  }
+
+}
+
 package android.hardware.devicestate {
 
   public final class DeviceStateManager {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index cb7b478..d6c10ae 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -81,8 +81,6 @@
 import android.util.DisplayMetrics;
 import android.util.Singleton;
 import android.util.Size;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.window.TaskSnapshot;
 
 import com.android.internal.app.LocalePicker;
@@ -92,6 +90,8 @@
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.MemInfoReader;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import java.io.FileDescriptor;
@@ -3953,6 +3953,10 @@
      * processes to reclaim memory; the system will take care of restarting
      * these processes in the future as needed.
      *
+     * <p class="note">On devices with a {@link Build.VERSION#SECURITY_PATCH} of 2022-12-01 or
+     * greater, third party applications can only use this API to kill their own processes.
+     * </p>
+     *
      * @param packageName The name of the package whose processes are to
      * be killed.
      */
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index b803070..4b1b0a2 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -221,6 +221,12 @@
     public abstract boolean isSystemReady();
 
     /**
+     * @return {@code true} if system is using the "modern" broadcast queue,
+     *         {@code false} otherwise.
+     */
+    public abstract boolean isModernQueueEnabled();
+
+    /**
      * Returns package name given pid.
      *
      * @param pid The pid we are searching package name for.
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 0cb00d9..3f39026 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -25,6 +25,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
@@ -1342,9 +1343,19 @@
     public static final int OP_RECEIVE_AMBIENT_TRIGGER_AUDIO =
             AppProtoEnums.APP_OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 
+     /**
+      * Receive audio from near-field mic (ie. TV remote)
+      * Allows audio recording regardless of sensor privacy state,
+      *  as it is an intentional user interaction: hold-to-talk
+      *
+      * @hide
+      */
+    public static final int OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO =
+            AppProtoEnums.APP_OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 121;
+    public static final int _NUM_OP = 122;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1816,6 +1827,18 @@
     public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO =
             "android:receive_ambient_trigger_audio";
 
+    /**
+     * Record audio from near-field microphone (ie. TV remote)
+     * Allows audio recording regardless of sensor privacy state,
+     *  as it is an intentional user interaction: hold-to-talk
+     *
+     * @hide
+     */
+    @SystemApi
+    @SuppressLint("IntentName")
+    public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO =
+            "android:receive_explicit_user_interaction_audio";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2285,7 +2308,11 @@
             .setDisableReset(true).setRestrictRead(true).build(),
         new AppOpInfo.Builder(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
                 "RECEIVE_SOUNDTRIGGER_AUDIO").setDefaultMode(AppOpsManager.MODE_ALLOWED)
-                .setForceCollectNotes(true).build()
+                .setForceCollectNotes(true).build(),
+        new AppOpInfo.Builder(OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
+                OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
+                "RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode(
+                AppOpsManager.MODE_ALLOWED).build()
     };
 
     /**
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index 13da190..cc4650a7 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -62,6 +62,7 @@
     private long mRequireCompatChangeId = CHANGE_INVALID;
     private boolean mRequireCompatChangeEnabled = true;
     private boolean mIsAlarmBroadcast = false;
+    private boolean mIsInteractiveBroadcast = false;
     private long mIdForResponseEvent;
     private @Nullable IntentFilter mRemoveMatchingFilter;
     private @DeliveryGroupPolicy int mDeliveryGroupPolicy;
@@ -168,6 +169,13 @@
             "android:broadcast.is_alarm";
 
     /**
+     * Corresponds to {@link #setInteractiveBroadcast(boolean)}
+     * @hide
+     */
+    public static final String KEY_INTERACTIVE_BROADCAST =
+            "android:broadcast.is_interactive";
+
+    /**
      * @hide
      * @deprecated Use {@link android.os.PowerExemptionManager#
      * TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED} instead.
@@ -281,6 +289,7 @@
         mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true);
         mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
         mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
+        mIsInteractiveBroadcast = opts.getBoolean(KEY_INTERACTIVE_BROADCAST, false);
         mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
                 IntentFilter.class);
         mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
@@ -599,6 +608,27 @@
     }
 
     /**
+     * When set, this broadcast will be understood as having originated from
+     * some direct interaction by the user such as a notification tap or button
+     * press.  Only the OS itself may use this option.
+     * @hide
+     * @param broadcastIsInteractive
+     * @see #isInteractiveBroadcast()
+     */
+    public void setInteractiveBroadcast(boolean broadcastIsInteractive) {
+        mIsInteractiveBroadcast = broadcastIsInteractive;
+    }
+
+    /**
+     * Did this broadcast originate with a direct user interaction?
+     * @return true if this broadcast is the result of an interaction, false otherwise
+     * @hide
+     */
+    public boolean isInteractiveBroadcast() {
+        return mIsInteractiveBroadcast;
+    }
+
+    /**
      * Did this broadcast originate from a push message from the server?
      *
      * @return true if this broadcast is a push message, false otherwise.
@@ -743,6 +773,9 @@
         if (mIsAlarmBroadcast) {
             b.putBoolean(KEY_ALARM_BROADCAST, true);
         }
+        if (mIsInteractiveBroadcast) {
+            b.putBoolean(KEY_INTERACTIVE_BROADCAST, true);
+        }
         if (mMinManifestReceiverApiLevel != 0) {
             b.putInt(KEY_MIN_MANIFEST_RECEIVER_API_LEVEL, mMinManifestReceiverApiLevel);
         }
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index be53a62..0e2b098 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -174,7 +174,7 @@
     @TestApi
     public void setSystemLocales(@NonNull LocaleList locales) {
         try {
-            Configuration conf = ActivityManager.getService().getConfiguration();
+            Configuration conf = new Configuration();
             conf.setLocales(locales);
             ActivityManager.getService().updatePersistentConfiguration(conf);
         } catch (RemoteException e) {
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 74eb1c5..f9ef3cc 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -8467,8 +8467,8 @@
             }
 
             int maxAvatarSize = resources.getDimensionPixelSize(
-                    isLowRam ? R.dimen.notification_person_icon_max_size
-                            : R.dimen.notification_person_icon_max_size_low_ram);
+                    isLowRam ? R.dimen.notification_person_icon_max_size_low_ram
+                            : R.dimen.notification_person_icon_max_size);
             if (mUser != null && mUser.getIcon() != null) {
                 mUser.getIcon().scaleDownIfNecessary(maxAvatarSize, maxAvatarSize);
             }
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 7215987..9615b68 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -33,12 +33,12 @@
 import android.service.notification.NotificationListenerService;
 import android.text.TextUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.json.JSONException;
 import org.json.JSONObject;
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index 5c29eb3..3bd86c1 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -23,10 +23,11 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 4ddfdb6..08a6b8c 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -85,6 +85,7 @@
 import android.credentials.ICredentialManager;
 import android.debug.AdbManager;
 import android.debug.IAdbManager;
+import android.devicelock.DeviceLockFrameworkInitializer;
 import android.graphics.fonts.FontManager;
 import android.hardware.ConsumerIrManager;
 import android.hardware.ISerialManager;
@@ -1555,6 +1556,7 @@
             ConnectivityFrameworkInitializerTiramisu.registerServiceWrappers();
             NearbyFrameworkInitializer.registerServiceWrappers();
             OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers();
+            DeviceLockFrameworkInitializer.registerServiceWrappers();
         } finally {
             // If any of the above code throws, we're in a pretty bad shape and the process
             // will likely crash, but we'll reset it just in case there's an exception handler...
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 5b0bd96..0f26818 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -175,6 +175,23 @@
             "file_patterns": [
                 "(/|^)KeyguardManager.java"
             ]
+        },
+        {
+            "name": "FrameworksCoreTests",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation": "org.junit.Ignore"
+                },
+                {
+                    "include-filter": "android.app.PropertyInvalidatedCacheTests"
+                }
+            ],
+            "file_patterns": [
+                "(/|^)PropertyInvalidatedCache.java"
+            ]
         }
     ],
     "presubmit-large": [
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 41256d0..67408a4 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -37,10 +37,11 @@
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/FactoryResetProtectionPolicy.java b/core/java/android/app/admin/FactoryResetProtectionPolicy.java
index 7e95177..efa23dd 100644
--- a/core/java/android/app/admin/FactoryResetProtectionPolicy.java
+++ b/core/java/android/app/admin/FactoryResetProtectionPolicy.java
@@ -27,8 +27,9 @@
 import android.os.Parcelable;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/ParcelableResource.java b/core/java/android/app/admin/ParcelableResource.java
index a297665..5b438f8 100644
--- a/core/java/android/app/admin/ParcelableResource.java
+++ b/core/java/android/app/admin/ParcelableResource.java
@@ -30,8 +30,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/PreferentialNetworkServiceConfig.java b/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
index 63c9839..b0ea499 100644
--- a/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
+++ b/core/java/android/app/admin/PreferentialNetworkServiceConfig.java
@@ -27,8 +27,9 @@
 import android.os.Parcelable;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/SystemUpdateInfo.java b/core/java/android/app/admin/SystemUpdateInfo.java
index b88bf76..9e6c91f 100644
--- a/core/java/android/app/admin/SystemUpdateInfo.java
+++ b/core/java/android/app/admin/SystemUpdateInfo.java
@@ -22,8 +22,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/admin/SystemUpdatePolicy.java b/core/java/android/app/admin/SystemUpdatePolicy.java
index 68ac4cc..b100eb2 100644
--- a/core/java/android/app/admin/SystemUpdatePolicy.java
+++ b/core/java/android/app/admin/SystemUpdatePolicy.java
@@ -26,8 +26,9 @@
 import android.os.Parcelable;
 import android.util.Log;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java
index 88a7c0f..d2c7972 100644
--- a/core/java/android/app/backup/BackupManager.java
+++ b/core/java/android/app/backup/BackupManager.java
@@ -29,7 +29,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -1123,18 +1122,4 @@
             });
         }
     }
-
-    private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
-        final BackupManagerMonitor mMonitor;
-
-        BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
-            mMonitor = monitor;
-        }
-
-        @Override
-        public void onEvent(final Bundle event) throws RemoteException {
-            mMonitor.onEvent(event);
-        }
-    }
-
 }
diff --git a/core/java/android/app/backup/BackupManagerMonitorWrapper.java b/core/java/android/app/backup/BackupManagerMonitorWrapper.java
new file mode 100644
index 0000000..0b18995
--- /dev/null
+++ b/core/java/android/app/backup/BackupManagerMonitorWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.backup;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+/**
+ * Wrapper around {@link BackupManagerMonitor} that helps with IPC between the caller of backup
+ * APIs and the backup service.
+ *
+ * The caller implements {@link BackupManagerMonitor} and passes it into framework APIs that run on
+ * the caller's process. Those framework APIs will then wrap it around this class when doing the
+ * actual IPC.
+ */
+class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
+    private final BackupManagerMonitor mMonitor;
+
+    BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
+        mMonitor = monitor;
+    }
+
+    @Override
+    public void onEvent(final Bundle event) throws RemoteException {
+        mMonitor.onEvent(event);
+    }
+}
diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java
index f6de72b..90e9df4 100644
--- a/core/java/android/app/backup/BackupTransport.java
+++ b/core/java/android/app/backup/BackupTransport.java
@@ -656,6 +656,20 @@
     }
 
     /**
+     * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the
+     * framework to report logging events back to the transport.
+     *
+     * <p>Backups requested from outside the framework may pass in a monitor with the request,
+     * however backups initiated by the framework will call this method to retrieve one.
+     *
+     * @hide
+     */
+    @Nullable
+    public BackupManagerMonitor getBackupManagerMonitor() {
+        return null;
+    }
+
+    /**
      * Bridge between the actual IBackupTransport implementation and the stable API.  If the
      * binder interface needs to change, we use this layer to translate so that we can
      * (if appropriate) decouple those framework-side changes from the BackupTransport
@@ -952,5 +966,15 @@
                 callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
             }
         }
+
+        @Override
+        public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture) {
+            try {
+                BackupManagerMonitor result = BackupTransport.this.getBackupManagerMonitor();
+                resultFuture.complete(new BackupManagerMonitorWrapper(result));
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
+        }
     }
 }
diff --git a/core/java/android/app/backup/RestoreSession.java b/core/java/android/app/backup/RestoreSession.java
index 9336704..fe68ec1 100644
--- a/core/java/android/app/backup/RestoreSession.java
+++ b/core/java/android/app/backup/RestoreSession.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.content.Context;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -393,17 +392,4 @@
                     mHandler.obtainMessage(MSG_RESTORE_FINISHED, error, 0));
         }
     }
-
-    private class BackupManagerMonitorWrapper extends IBackupManagerMonitor.Stub {
-        final BackupManagerMonitor mMonitor;
-
-        BackupManagerMonitorWrapper(BackupManagerMonitor monitor) {
-            mMonitor = monitor;
-        }
-
-        @Override
-        public void onEvent(final Bundle event) throws RemoteException {
-            mMonitor.onEvent(event);
-        }
-    }
 }
diff --git a/core/java/android/app/time/TimeCapabilities.java b/core/java/android/app/time/TimeCapabilities.java
index 76bad58..752caac 100644
--- a/core/java/android/app/time/TimeCapabilities.java
+++ b/core/java/android/app/time/TimeCapabilities.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.app.time.Capabilities.CapabilityState;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -37,6 +38,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeCapabilities implements Parcelable {
 
     public static final @NonNull Creator<TimeCapabilities> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/TimeCapabilitiesAndConfig.java b/core/java/android/app/time/TimeCapabilitiesAndConfig.java
index b6a0818..c9a45e0 100644
--- a/core/java/android/app/time/TimeCapabilitiesAndConfig.java
+++ b/core/java/android/app/time/TimeCapabilitiesAndConfig.java
@@ -17,6 +17,7 @@
 package android.app.time;
 
 import android.annotation.NonNull;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -27,6 +28,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeCapabilitiesAndConfig implements Parcelable {
 
     public static final @NonNull Creator<TimeCapabilitiesAndConfig> CREATOR =
diff --git a/core/java/android/app/time/TimeConfiguration.java b/core/java/android/app/time/TimeConfiguration.java
index 7d98698..048f85a 100644
--- a/core/java/android/app/time/TimeConfiguration.java
+++ b/core/java/android/app/time/TimeConfiguration.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.StringDef;
+import android.annotation.SystemApi;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -40,6 +41,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeConfiguration implements Parcelable {
 
     public static final @NonNull Creator<TimeConfiguration> CREATOR =
@@ -155,6 +157,7 @@
      *
      * @hide
      */
+    @SystemApi
     public static final class Builder {
 
         private final Bundle mBundle = new Bundle();
diff --git a/core/java/android/app/time/TimeManager.java b/core/java/android/app/time/TimeManager.java
index 9f66f09..e35e359 100644
--- a/core/java/android/app/time/TimeManager.java
+++ b/core/java/android/app/time/TimeManager.java
@@ -88,8 +88,6 @@
 
     /**
      * Returns the calling user's time capabilities and configuration.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -107,10 +105,26 @@
     /**
      * Modifies the time detection configuration.
      *
-     * @return {@code true} if all the configuration settings specified have been set to the
-     * new values, {@code false} if none have
+     * <p>The ability to modify configuration settings can be subject to restrictions. For
+     * example, they may be determined by device hardware, general policy (i.e. only the primary
+     * user can set them), or by a managed device policy. Use {@link
+     * #getTimeCapabilitiesAndConfig()} to obtain information at runtime about the user's
+     * capabilities.
      *
-     * @hide
+     * <p>Attempts to modify configuration settings with capabilities that are {@link
+     * Capabilities#CAPABILITY_NOT_SUPPORTED} or {@link
+     * Capabilities#CAPABILITY_NOT_ALLOWED} will have no effect and a {@code false}
+     * will be returned. Modifying configuration settings with capabilities that are {@link
+     * Capabilities#CAPABILITY_NOT_APPLICABLE} or {@link
+     * Capabilities#CAPABILITY_POSSESSED} will succeed. See {@link
+     * TimeZoneCapabilities} for further details.
+     *
+     * <p>If the supplied configuration only has some values set, then only the specified settings
+     * will be updated (where the user's capabilities allow) and other settings will be left
+     * unchanged.
+     *
+     * @return {@code true} if all the configuration settings specified have been set to the
+     *   new values, {@code false} if none have
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean updateTimeConfiguration(@NonNull TimeConfiguration configuration) {
@@ -280,8 +294,6 @@
     /**
      * Returns a snapshot of the device's current system clock time state. See also {@link
      * #confirmTime(UnixEpochTime)} for how this information can be used.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -306,8 +318,6 @@
      * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time being
      * confirmed is no longer the time the device is currently set to. Confirming a time
      * in which the system already has high confidence will return {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean confirmTime(@NonNull UnixEpochTime unixEpochTime) {
@@ -329,8 +339,6 @@
      * capabilities prevents the time being accepted, e.g. if the device is currently set to
      * "automatic time detection". This method returns {@code true} if the time was accepted even
      * if it is the same as the current device time.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean setManualTime(@NonNull UnixEpochTime unixEpochTime) {
@@ -353,8 +361,6 @@
      * Returns a snapshot of the device's current time zone state. See also {@link
      * #confirmTimeZone(String)} and {@link #setManualTimeZone(String)} for how this information may
      * be used.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     @NonNull
@@ -379,8 +385,6 @@
      * <p>Returns {@code false} if the confirmation is invalid, i.e. if the time zone ID being
      * confirmed is no longer the time zone ID the device is currently set to. Confirming a time
      * zone ID in which the system already has high confidence returns {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean confirmTimeZone(@NonNull String timeZoneId) {
@@ -402,8 +406,6 @@
      * capabilities prevents the time zone being accepted, e.g. if the device is currently set to
      * "automatic time zone detection". {@code true} is returned if the time zone is accepted. A
      * time zone that is accepted and matches the current device time zone returns {@code true}.
-     *
-     * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_TIME_AND_ZONE_DETECTION)
     public boolean setManualTimeZone(@NonNull String timeZoneId) {
diff --git a/core/java/android/app/time/TimeState.java b/core/java/android/app/time/TimeState.java
index 01c869d..c209cde 100644
--- a/core/java/android/app/time/TimeState.java
+++ b/core/java/android/app/time/TimeState.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -36,6 +37,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeState implements Parcelable {
 
     public static final @NonNull Creator<TimeState> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java
index 2f147ce..b647fc3 100644
--- a/core/java/android/app/time/TimeZoneCapabilities.java
+++ b/core/java/android/app/time/TimeZoneCapabilities.java
@@ -114,8 +114,6 @@
      * <p>The time zone will be ignored in all cases unless the value is {@link
      * Capabilities#CAPABILITY_POSSESSED}. See also
      * {@link TimeZoneConfiguration#isAutoDetectionEnabled()}.
-     *
-     * @hide
      */
     @CapabilityState
     public int getSetManualTimeZoneCapability() {
diff --git a/core/java/android/app/time/TimeZoneState.java b/core/java/android/app/time/TimeZoneState.java
index 8e87111..beb6dc6 100644
--- a/core/java/android/app/time/TimeZoneState.java
+++ b/core/java/android/app/time/TimeZoneState.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -36,6 +37,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class TimeZoneState implements Parcelable {
 
     public static final @NonNull Creator<TimeZoneState> CREATOR = new Creator<>() {
diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java
index 576bf64..3a35f3c 100644
--- a/core/java/android/app/time/UnixEpochTime.java
+++ b/core/java/android/app/time/UnixEpochTime.java
@@ -19,6 +19,7 @@
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.ShellCommand;
@@ -38,6 +39,7 @@
  *
  * @hide
  */
+@SystemApi
 public final class UnixEpochTime implements Parcelable {
     @ElapsedRealtimeLong private final long mElapsedRealtimeMillis;
     private final long mUnixEpochTimeMillis;
@@ -153,9 +155,8 @@
      * Creates a new Unix epoch time value at {@code elapsedRealtimeTimeMillis} by adjusting this
      * Unix epoch time by the difference between the elapsed realtime value supplied and the one
      * associated with this instance.
-     *
-     * @hide
      */
+    @NonNull
     public UnixEpochTime at(@ElapsedRealtimeLong long elapsedRealtimeTimeMillis) {
         long adjustedUnixEpochTimeMillis =
                 (elapsedRealtimeTimeMillis - mElapsedRealtimeMillis) + mUnixEpochTimeMillis;
diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java
index cc303fb..2dced96 100644
--- a/core/java/android/appwidget/AppWidgetHost.java
+++ b/core/java/android/appwidget/AppWidgetHost.java
@@ -329,6 +329,22 @@
     }
 
     /**
+     * Set the visibiity of all widgets associated with this host to hidden
+     *
+     * @hide
+     */
+    public void setAppWidgetHidden() {
+        if (sService == null) {
+            return;
+        }
+        try {
+            sService.setAppWidgetHidden(mContextOpPackageName, mHostId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("System server dead?", e);
+        }
+    }
+
+    /**
      * Set the host's interaction handler.
      *
      * @hide
@@ -418,14 +434,7 @@
         AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget);
         view.setInteractionHandler(mInteractionHandler);
         view.setAppWidget(appWidgetId, appWidget);
-        addListener(appWidgetId, view);
-        RemoteViews views;
-        try {
-            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
-        } catch (RemoteException e) {
-            throw new RuntimeException("system server dead?", e);
-        }
-        view.updateAppWidget(views);
+        setListener(appWidgetId, view);
 
         return view;
     }
@@ -513,13 +522,19 @@
      * The AppWidgetHost retains a pointer to the newly-created listener.
      * @param appWidgetId The ID of the app widget for which to add the listener
      * @param listener The listener interface that deals with actions towards the widget view
-     *
      * @hide
      */
-    public void addListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
+    public void setListener(int appWidgetId, @NonNull AppWidgetHostListener listener) {
         synchronized (mListeners) {
             mListeners.put(appWidgetId, listener);
         }
+        RemoteViews views = null;
+        try {
+            views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId);
+        } catch (RemoteException e) {
+            throw new RuntimeException("system server dead?", e);
+        }
+        listener.updateAppWidget(views);
     }
 
     /**
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index e7f19166..295d69d 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -88,6 +88,7 @@
             IBinder token,
             in Point screenSize);
     void unregisterInputDevice(IBinder token);
+    int getInputDeviceId(IBinder token);
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 753c936..d65210b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3938,6 +3938,7 @@
             //@hide: SAFETY_CENTER_SERVICE,
             DISPLAY_HASH_SERVICE,
             CREDENTIAL_SERVICE,
+            DEVICE_LOCK_SERVICE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ServiceName {}
@@ -6073,6 +6074,14 @@
     public static final String CREDENTIAL_SERVICE = "credential";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.devicelock.DeviceLockManager}.
+     *
+     * @see #getSystemService(String)
+     */
+    public static final String DEVICE_LOCK_SERVICE = "device_lock";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/SyncAdaptersCache.java b/core/java/android/content/SyncAdaptersCache.java
index 495f94f..bf9dc8e 100644
--- a/core/java/android/content/SyncAdaptersCache.java
+++ b/core/java/android/content/SyncAdaptersCache.java
@@ -25,10 +25,10 @@
 import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 26c947b..fda4119 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -221,6 +221,23 @@
     public String launchToken;
 
     /**
+     * Specifies the category of the target display the activity is expected to run on. Set from
+     * the {@link android.R.attr#targetDisplayCategory} attribute. Upon creation, a virtual display
+     * can specify which display categories it supports and one of the category must be present in
+     * the activity's manifest to allow this activity to run. The default value is {@code null},
+     * which indicates the activity does not belong to a restricted display category and thus can
+     * only run on a display that didn't specify any display categories. Each activity can only
+     * specify one category it targets to but a virtual display can support multiple restricted
+     * categories.
+     *
+     * This field should be formatted as a Java-language-style free form string(for example,
+     * com.google.automotive_entertainment), which may contain uppercase or lowercase letters ('A'
+     * through 'Z'), numbers, and underscores ('_') but may only start with letters.
+     */
+    @Nullable
+    public String targetDisplayCategory;
+
+    /**
      * Activity can not be resized and always occupies the fullscreen area with all windows fully
      * visible.
      * @hide
@@ -1313,6 +1330,7 @@
         mMaxAspectRatio = orig.mMaxAspectRatio;
         mMinAspectRatio = orig.mMinAspectRatio;
         supportsSizeChanges = orig.supportsSizeChanges;
+        targetDisplayCategory = orig.targetDisplayCategory;
     }
 
     /**
@@ -1651,6 +1669,9 @@
         if (mKnownActivityEmbeddingCerts != null) {
             pw.println(prefix + "knownActivityEmbeddingCerts=" + mKnownActivityEmbeddingCerts);
         }
+        if (targetDisplayCategory != null) {
+            pw.println(prefix + "targetDisplayCategory=" + targetDisplayCategory);
+        }
         super.dumpBack(pw, prefix, dumpFlags);
     }
 
@@ -1697,6 +1718,7 @@
         dest.writeFloat(mMinAspectRatio);
         dest.writeBoolean(supportsSizeChanges);
         sForStringSet.parcel(mKnownActivityEmbeddingCerts, dest, flags);
+        dest.writeString8(targetDisplayCategory);
     }
 
     /**
@@ -1822,6 +1844,7 @@
         if (mKnownActivityEmbeddingCerts.isEmpty()) {
             mKnownActivityEmbeddingCerts = null;
         }
+        targetDisplayCategory = source.readString8();
     }
 
     /**
diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.java b/core/java/android/content/pm/IntentFilterVerificationInfo.java
index 56b8bd8..2b40fdf 100644
--- a/core/java/android/content/pm/IntentFilterVerificationInfo.java
+++ b/core/java/android/content/pm/IntentFilterVerificationInfo.java
@@ -28,10 +28,10 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index db991dc..aa86af9 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2205,6 +2205,13 @@
      */
     public static final int INSTALL_ACTIVATION_FAILED = -128;
 
+    /**
+     * Installation failed return code: requesting user pre-approval is currently unavailable.
+     *
+     * @hide
+     */
+    public static final int INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE = -129;
+
     /** @hide */
     @IntDef(flag = true, prefix = { "DELETE_" }, value = {
             DELETE_KEEP_DATA,
@@ -4194,6 +4201,14 @@
     @SdkConstant(SdkConstantType.FEATURE)
     public static final String FEATURE_CREDENTIALS = "android.software.credentials";
 
+    /**
+     * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
+     * The device supports locking (for example, by a financing provider in case of a missed
+     * payment).
+     */
+    @SdkConstant(SdkConstantType.FEATURE)
+    public static final String FEATURE_DEVICE_LOCK = "android.software.device_lock";
+
     /** @hide */
     public static final boolean APP_ENUMERATION_ENABLED_BY_DEFAULT = true;
 
@@ -9635,6 +9650,7 @@
             case INSTALL_FAILED_NO_MATCHING_ABIS: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
             case INSTALL_FAILED_ABORTED: return PackageInstaller.STATUS_FAILURE_ABORTED;
             case INSTALL_FAILED_MISSING_SPLIT: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
+            case INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE: return PackageInstaller.STATUS_FAILURE_BLOCKED;
             default: return PackageInstaller.STATUS_FAILURE;
         }
     }
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 78984bd..104527e 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -36,14 +36,14 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java
index d94b0d8..b049880 100644
--- a/core/java/android/content/pm/Signature.java
+++ b/core/java/android/content/pm/Signature.java
@@ -21,9 +21,9 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
diff --git a/core/java/android/content/pm/SuspendDialogInfo.java b/core/java/android/content/pm/SuspendDialogInfo.java
index 23945ee..8786f7c 100644
--- a/core/java/android/content/pm/SuspendDialogInfo.java
+++ b/core/java/android/content/pm/SuspendDialogInfo.java
@@ -29,11 +29,11 @@
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.IOException;
 import java.lang.annotation.Retention;
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index 1a82e4d..645a1ac 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -22,10 +22,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -114,7 +114,7 @@
     public UserProperties(UserProperties orig,
             boolean exposeAllFields,
             boolean hasManagePermission,
-            boolean hasQueryPermission) {
+            boolean hasQueryOrManagePermission) {
 
         if (orig.mDefaultProperties == null) {
             throw new IllegalArgumentException("Attempting to copy a non-original UserProperties.");
@@ -122,17 +122,19 @@
 
         this.mDefaultProperties = null;
 
+        // Insert each setter into the following hierarchy based on its permission requirements.
         // NOTE: Copy each property using getters to ensure default values are copied if needed.
         if (exposeAllFields) {
+            // Add items that require exposeAllFields to be true (strictest permission level).
             setStartWithParent(orig.getStartWithParent());
         }
         if (hasManagePermission) {
-            // Add any items that require this permission.
+            // Add items that require MANAGE_USERS or stronger.
         }
-        if (hasQueryPermission) {
-            // Add any items that require this permission.
+        if (hasQueryOrManagePermission) {
+            // Add items that require QUERY_USERS or stronger.
         }
-        // Add any items that require no permissions at all.
+        // Add items that have no permission requirements at all.
         setShowInLauncher(orig.getShowInLauncher());
     }
 
diff --git a/core/java/android/content/pm/XmlSerializerAndParser.java b/core/java/android/content/pm/XmlSerializerAndParser.java
index 51cd6ca..d748aa1 100644
--- a/core/java/android/content/pm/XmlSerializerAndParser.java
+++ b/core/java/android/content/pm/XmlSerializerAndParser.java
@@ -17,10 +17,10 @@
 package android.content.pm;
 
 import android.compat.annotation.UnsupportedAppUsage;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/credentials/Credential.java b/core/java/android/credentials/Credential.java
index a247d16..fed2592 100644
--- a/core/java/android/credentials/Credential.java
+++ b/core/java/android/credentials/Credential.java
@@ -32,6 +32,13 @@
 public final class Credential implements Parcelable {
 
     /**
+     * The type value for password credential related operations.
+     *
+     * @hide
+     */
+    @NonNull public static final String TYPE_PASSWORD = "android.credentials.TYPE_PASSWORD";
+
+    /**
      * The credential type.
      */
     @NonNull
diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java
new file mode 100644
index 0000000..cf5f036
--- /dev/null
+++ b/core/java/android/credentials/ui/BaseDialogResult.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * Base dialog result data.
+ *
+ * Returned for simple use cases like cancellation. Can also be subclassed when more information
+ * is needed, e.g. {@link UserSelectionDialogResult}.
+ *
+ * @hide
+ */
+public class BaseDialogResult implements Parcelable {
+    /** Parses and returns a BaseDialogResult from the given resultData. */
+    @Nullable
+    public static BaseDialogResult fromResultData(@NonNull Bundle resultData) {
+        return resultData.getParcelable(EXTRA_BASE_RESULT, BaseDialogResult.class);
+    }
+
+    /**
+     * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+     *  ResultReceiver}.
+     */
+    public static void addToBundle(@NonNull BaseDialogResult result, @NonNull Bundle bundle) {
+        bundle.putParcelable(EXTRA_BASE_RESULT, result);
+    }
+
+    /**
+     * The intent extra key for the {@code BaseDialogResult} object when the credential
+     * selector activity finishes.
+     */
+    private static final String EXTRA_BASE_RESULT =
+            "android.credentials.ui.extra.BASE_RESULT";
+
+    /** User intentionally canceled the dialog. */
+    public static final int RESULT_CODE_DIALOG_CANCELED = 0;
+    /**
+     * User made a selection and the dialog finished. The user selection result is in the
+     * {@code resultData}.
+     */
+    public static final int RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION = 1;
+    /**
+     * The user has acknowledged the consent page rendered for when they first used Credential
+     * Manager on this device.
+     */
+    public static final int RESULT_CODE_CREDENTIAL_MANAGER_CONSENT_ACKNOWLEDGED = 2;
+    /**
+     * The user has acknowledged the consent page rendered for enabling a new provider.
+     * This should only happen during the first time use. The provider info is in the
+     * {@code resultData}.
+     */
+    public static final int RESULT_CODE_PROVIDER_ENABLED = 3;
+    /**
+     * The user has consented to switching to a new default provider. The provider info is in the
+     * {@code resultData}.
+     */
+    public static final int RESULT_CODE_DEFAULT_PROVIDER_CHANGED = 4;
+
+    @NonNull
+    private final IBinder mRequestToken;
+
+    public BaseDialogResult(@NonNull IBinder requestToken) {
+        mRequestToken = requestToken;
+    }
+
+    /** Returns the unique identifier for the request that launched the operation. */
+    @NonNull
+    public IBinder getRequestToken() {
+        return mRequestToken;
+    }
+
+    protected BaseDialogResult(@NonNull Parcel in) {
+        IBinder requestToken = in.readStrongBinder();
+        mRequestToken = requestToken;
+        AnnotationValidations.validate(NonNull.class, null, mRequestToken);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mRequestToken);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<BaseDialogResult> CREATOR =
+            new Creator<BaseDialogResult>() {
+        @Override
+        public BaseDialogResult createFromParcel(@NonNull Parcel in) {
+            return new BaseDialogResult(in);
+        }
+
+        @Override
+        public BaseDialogResult[] newArray(int size) {
+            return new BaseDialogResult[size];
+        }
+    };
+}
diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java
index aeeede7..53ad40d 100644
--- a/core/java/android/credentials/ui/Constants.java
+++ b/core/java/android/credentials/ui/Constants.java
@@ -29,5 +29,4 @@
     */
     public static final String EXTRA_RESULT_RECEIVER =
             "android.credentials.ui.extra.RESULT_RECEIVER";
-
 }
diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
index 122c54a..33427d3 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -30,12 +30,39 @@
  * @hide
  */
 public class Entry implements Parcelable {
-    // TODO: move to jetpack.
+    // TODO: these constants should go to jetpack.
     public static final String VERSION = "v1";
     public static final Uri CREDENTIAL_MANAGER_ENTRY_URI = Uri.parse("credentialmanager.slice");
-    public static final String HINT_TITLE = "hint_title";
-    public static final String HINT_SUBTITLE = "hint_subtitle";
-    public static final String HINT_ICON = "hint_icon";
+    // TODO: remove these hint constants and use the credential entry & action ones defined below.
+    public static final String HINT_TITLE = "HINT_TITLE";
+    public static final String HINT_SUBTITLE = "HINT_SUBTITLE";
+    public static final String HINT_ICON = "HINT_ICON";
+    /**
+     * 1. CREDENTIAL ENTRY CONSTANTS
+     */
+    // User profile picture associated with this credential entry.
+    public static final String HINT_PROFILE_ICON = "HINT_PROFILE_ICON";
+    public static final String HINT_CREDENTIAL_TYPE_ICON = "HINT_CREDENTIAL_TYPE_ICON";
+     // The user account name of this provider app associated with this entry.
+     // Note: this is independent from the request app.
+    public static final String HINT_USER_PROVIDER_ACCOUNT_NAME = "HINT_USER_PROVIDER_ACCOUNT_NAME";
+    public static final String HINT_PASSWORD_COUNT = "HINT_PASSWORD_COUNT";
+    public static final String HINT_PASSKEY_COUNT = "HINT_PASSKEY_COUNT";
+    public static final String HINT_TOTAL_CREDENTIAL_COUNT = "HINT_TOTAL_CREDENTIAL_COUNT";
+    public static final String HINT_LAST_USED_TIME_MILLIS = "HINT_LAST_USED_TIME_MILLIS";
+    /** Below are only available for get flows. */
+    public static final String HINT_NOTE = "HINT_NOTE";
+    public static final String HINT_USER_NAME = "HINT_USER_NAME";
+    public static final String HINT_CREDENTIAL_TYPE = "HINT_CREDENTIAL_TYPE";
+    public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME";
+    public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE";
+
+    /**
+     * 2. ACTION CONSTANTS
+     */
+    public static final String HINT_ACTION_TITLE = "HINT_ACTION_TITLE";
+    public static final String HINT_ACTION_SUBTEXT = "HINT_ACTION_SUBTEXT";
+    public static final String HINT_ACTION_ICON = "HINT_ACTION_ICON";
 
     /**
     * The intent extra key for the action chip {@code Entry} list when launching the UX activities.
@@ -55,32 +82,46 @@
     public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION =
             "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION";
 
-    // TODO: may be changed to other type depending on the service implementation.
-    private final int mId;
+    @NonNull private final String mKey;
+    @NonNull private final String mSubkey;
 
     @NonNull
     private final Slice mSlice;
 
     protected Entry(@NonNull Parcel in) {
-        int entryId = in.readInt();
+        String key = in.readString8();
+        String subkey = in.readString8();
         Slice slice = Slice.CREATOR.createFromParcel(in);
 
-        mId = entryId;
+        mKey = key;
+        AnnotationValidations.validate(NonNull.class, null, mKey);
+        mSubkey = subkey;
+        AnnotationValidations.validate(NonNull.class, null, mSubkey);
         mSlice = slice;
         AnnotationValidations.validate(NonNull.class, null, mSlice);
     }
 
-    public Entry(int id, @NonNull Slice slice) {
-        mId = id;
+    public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice) {
+        mKey = key;
+        mSubkey = subkey;
         mSlice = slice;
     }
 
     /**
-    * Returns the id of this entry that's unique within the context of the CredentialManager
+    * Returns the identifier of this entry that's unique within the context of the CredentialManager
     * request.
     */
-    public int getEntryId() {
-        return mId;
+    @NonNull
+    public String getKey() {
+        return mKey;
+    }
+
+    /**
+     * Returns the sub-identifier of this entry that's unique within the context of the {@code key}.
+     */
+    @NonNull
+    public String getSubkey() {
+        return mSubkey;
     }
 
     /**
@@ -93,7 +134,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeInt(mId);
+        dest.writeString8(mKey);
+        dest.writeString8(mSubkey);
         mSlice.writeToParcel(dest, flags);
     }
 
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
index 9a038d1..1b70ea4 100644
--- a/core/java/android/credentials/ui/IntentFactory.java
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -34,8 +34,7 @@
             ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) {
         Intent intent = new Intent();
         // TODO: define these as proper config strings.
-        String activityName = "com.androidauth.tatiaccountselector/.CredentialSelectorActivity";
-        // String activityName = "com.android.credentialmanager/.CredentialSelectorActivity";
+        String activityName = "com.android.credentialmanager/.CredentialSelectorActivity";
         intent.setComponent(ComponentName.unflattenFromString(activityName));
 
         intent.putParcelableArrayListExtra(
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 38bd4e5..35e12fa 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -16,8 +16,10 @@
 
 package android.credentials.ui;
 
+import android.annotation.CurrentTimeMillisLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.drawable.Icon;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -43,30 +45,49 @@
     @NonNull
     private final String mProviderId;
     @NonNull
+    private final String mProviderDisplayName;
+    @NonNull
+    private final Icon mIcon;
+    @NonNull
     private final List<Entry> mCredentialEntries;
     @NonNull
     private final List<Entry> mActionChips;
     @Nullable
     private final Entry mAuthenticationEntry;
 
+    private final @CurrentTimeMillisLong long mLastUsedTimeMillis;
+
     public ProviderData(
-            @NonNull String providerId,
-            @NonNull List<Entry> credentialEntries,
-            @NonNull List<Entry> actionChips,
-            @Nullable Entry authenticationEntry) {
+            @NonNull String providerId, @NonNull String providerDisplayName,
+            @NonNull Icon icon, @NonNull List<Entry> credentialEntries,
+            @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
+            @CurrentTimeMillisLong long lastUsedTimeMillis) {
         mProviderId = providerId;
+        mProviderDisplayName = providerDisplayName;
+        mIcon = icon;
         mCredentialEntries = credentialEntries;
         mActionChips = actionChips;
         mAuthenticationEntry = authenticationEntry;
+        mLastUsedTimeMillis = lastUsedTimeMillis;
     }
 
-    /** Returns the provider package name. */
+    /** Returns the unique provider id. */
     @NonNull
     public String getProviderId() {
         return mProviderId;
     }
 
     @NonNull
+    public String getProviderDisplayName() {
+        return mProviderDisplayName;
+    }
+
+    @NonNull
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    @NonNull
     public List<Entry> getCredentialEntries() {
         return mCredentialEntries;
     }
@@ -81,11 +102,24 @@
         return mAuthenticationEntry;
     }
 
+    /** Returns the time when the provider was last used. */
+    public @CurrentTimeMillisLong long getLastUsedTimeMillis() {
+        return mLastUsedTimeMillis;
+    }
+
     protected ProviderData(@NonNull Parcel in) {
         String providerId = in.readString8();
         mProviderId = providerId;
         AnnotationValidations.validate(NonNull.class, null, mProviderId);
 
+        String providerDisplayName = in.readString8();
+        mProviderDisplayName = providerDisplayName;
+        AnnotationValidations.validate(NonNull.class, null, mProviderDisplayName);
+
+        Icon icon = in.readTypedObject(Icon.CREATOR);
+        mIcon = icon;
+        AnnotationValidations.validate(NonNull.class, null, mIcon);
+
         List<Entry> credentialEntries = new ArrayList<>();
         in.readTypedList(credentialEntries, Entry.CREATOR);
         mCredentialEntries = credentialEntries;
@@ -98,14 +132,20 @@
 
         Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
         mAuthenticationEntry = authenticationEntry;
+
+        long lastUsedTimeMillis = in.readLong();
+        mLastUsedTimeMillis = lastUsedTimeMillis;
     }
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mProviderId);
+        dest.writeString8(mProviderDisplayName);
+        dest.writeTypedObject(mIcon, flags);
         dest.writeTypedList(mCredentialEntries);
         dest.writeTypedList(mActionChips);
         dest.writeTypedObject(mAuthenticationEntry, flags);
+        dest.writeLong(mLastUsedTimeMillis);
     }
 
     @Override
@@ -124,4 +164,83 @@
             return new ProviderData[size];
         }
     };
+
+    /**
+     * Builder for {@link ProviderData}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        private @NonNull String mProviderId;
+        private @NonNull String mProviderDisplayName;
+        private @NonNull Icon mIcon;
+        private @NonNull List<Entry> mCredentialEntries = new ArrayList<>();
+        private @NonNull List<Entry> mActionChips = new ArrayList<>();
+        private @Nullable Entry mAuthenticationEntry = null;
+        private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L;
+
+        /** Constructor with required properties. */
+        public Builder(@NonNull String providerId, @NonNull String providerDisplayName,
+                @NonNull Icon icon) {
+            mProviderId = providerId;
+            mProviderDisplayName = providerDisplayName;
+            mIcon = icon;
+        }
+
+        /** Sets the unique provider id. */
+        @NonNull
+        public Builder setProviderId(@NonNull String providerId) {
+            mProviderId = providerId;
+            return this;
+        }
+
+        /** Sets the provider display name to be displayed to the user. */
+        @NonNull
+        public Builder setProviderDisplayName(@NonNull String providerDisplayName) {
+            mProviderDisplayName = providerDisplayName;
+            return this;
+        }
+
+        /** Sets the provider icon to be displayed to the user. */
+        @NonNull
+        public Builder setIcon(@NonNull Icon icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /** Sets the list of save / get credential entries to be displayed to the user. */
+        @NonNull
+        public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
+            mCredentialEntries = credentialEntries;
+            return this;
+        }
+
+        /** Sets the list of action chips to be displayed to the user. */
+        @NonNull
+        public Builder setActionChips(@NonNull List<Entry> actionChips) {
+            mActionChips = actionChips;
+            return this;
+        }
+
+        /** Sets the authentication entry to be displayed to the user. */
+        @NonNull
+        public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) {
+            mAuthenticationEntry = authenticationEntry;
+            return this;
+        }
+
+        /** Sets the time when the provider was last used. */
+        @NonNull
+        public Builder setLastUsedTimeMillis(@CurrentTimeMillisLong long lastUsedTimeMillis) {
+            mLastUsedTimeMillis = lastUsedTimeMillis;
+            return this;
+        }
+
+        /** Builds a {@link ProviderData}. */
+        @NonNull
+        public ProviderData build() {
+            return new ProviderData(mProviderId, mProviderDisplayName, mIcon, mCredentialEntries,
+                mActionChips, mAuthenticationEntry, mLastUsedTimeMillis);
+        }
+    }
 }
diff --git a/core/java/android/credentials/ui/ProviderDialogResult.java b/core/java/android/credentials/ui/ProviderDialogResult.java
new file mode 100644
index 0000000..9d1be20
--- /dev/null
+++ b/core/java/android/credentials/ui/ProviderDialogResult.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * Result data matching {@link BaseDialogResult#RESULT_CODE_PROVIDER_ENABLED}, or {@link
+ * BaseDialogResult#RESULT_CODE_DEFAULT_PROVIDER_CHANGED}.
+ *
+ * @hide
+ */
+public class ProviderDialogResult extends BaseDialogResult implements Parcelable {
+    /** Parses and returns a ProviderDialogResult from the given resultData. */
+    @Nullable
+    public static ProviderDialogResult fromResultData(@NonNull Bundle resultData) {
+        return resultData.getParcelable(EXTRA_PROVIDER_RESULT, ProviderDialogResult.class);
+    }
+
+    /**
+     * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+     *  ResultReceiver}.
+     */
+    public static void addToBundle(
+            @NonNull ProviderDialogResult result, @NonNull Bundle bundle) {
+        bundle.putParcelable(EXTRA_PROVIDER_RESULT, result);
+    }
+
+    /**
+     * The intent extra key for the {@code ProviderDialogResult} object when the credential
+     * selector activity finishes.
+     */
+    private static final String EXTRA_PROVIDER_RESULT =
+            "android.credentials.ui.extra.PROVIDER_RESULT";
+
+    @NonNull
+    private final String mProviderId;
+
+    public ProviderDialogResult(@NonNull IBinder requestToken, @NonNull String providerId) {
+        super(requestToken);
+        mProviderId = providerId;
+    }
+
+    @NonNull
+    public String getProviderId() {
+        return mProviderId;
+    }
+
+    protected ProviderDialogResult(@NonNull Parcel in) {
+        super(in);
+        String providerId = in.readString8();
+        mProviderId = providerId;
+        AnnotationValidations.validate(NonNull.class, null, mProviderId);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString8(mProviderId);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<ProviderDialogResult> CREATOR =
+            new Creator<ProviderDialogResult>() {
+        @Override
+        public ProviderDialogResult createFromParcel(@NonNull Parcel in) {
+            return new ProviderDialogResult(in);
+        }
+
+        @Override
+        public ProviderDialogResult[] newArray(int size) {
+            return new ProviderDialogResult[size];
+        }
+    };
+}
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index eddb519..619b08e 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -17,12 +17,19 @@
 package android.credentials.ui;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.credentials.CreateCredentialRequest;
+import android.credentials.GetCredentialRequest;
 import android.os.IBinder;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import com.android.internal.util.AnnotationValidations;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Contains information about the request that initiated this UX flow.
  *
@@ -42,18 +49,45 @@
     /** Type value for an executeCreateCredential request. */
     public static final @NonNull String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @StringDef(value = { TYPE_GET, TYPE_CREATE })
+    public @interface RequestType {}
+
     @NonNull
     private final IBinder mToken;
 
+    @Nullable
+    private final CreateCredentialRequest mCreateCredentialRequest;
+
+    @Nullable
+    private final GetCredentialRequest mGetCredentialRequest;
+
     @NonNull
+    @RequestType
     private final String mType;
 
     private final boolean mIsFirstUsage;
 
-    public RequestInfo(@NonNull IBinder token, @NonNull String type, boolean isFirstUsage) {
-        mToken = token;
-        mType = type;
-        mIsFirstUsage = isFirstUsage;
+    @NonNull
+    private final String mAppDisplayName;
+
+    /** Creates new {@code RequestInfo} for a create-credential flow. */
+    public static RequestInfo newCreateRequestInfo(
+            @NonNull IBinder token, @NonNull CreateCredentialRequest createCredentialRequest,
+            boolean isFirstUsage, @NonNull String appDisplayName) {
+        return new RequestInfo(
+                token, TYPE_CREATE, isFirstUsage, appDisplayName,
+                createCredentialRequest, null);
+    }
+
+    /** Creates new {@code RequestInfo} for a get-credential flow. */
+    public static RequestInfo newGetRequestInfo(
+            @NonNull IBinder token, @NonNull GetCredentialRequest getCredentialRequest,
+            boolean isFirstUsage, @NonNull String appDisplayName) {
+        return new RequestInfo(
+                token, TYPE_GET, isFirstUsage, appDisplayName,
+                null, getCredentialRequest);
     }
 
     /** Returns the request token matching the user request. */
@@ -64,6 +98,7 @@
 
     /** Returns the request type. */
     @NonNull
+    @RequestType
     public String getType() {
         return mType;
     }
@@ -78,16 +113,61 @@
         return mIsFirstUsage;
     }
 
+    /** Returns the display name of the app that made this request. */
+    @NonNull
+    public String getAppDisplayName() {
+        return mAppDisplayName;
+    }
+
+    /**
+     * Returns the non-null CreateCredentialRequest when the type of the request is {@link
+     * #TYPE_CREATE}, or null otherwise.
+     */
+    @Nullable
+    public CreateCredentialRequest getCreateCredentialRequest() {
+        return mCreateCredentialRequest;
+    }
+
+    /**
+     * Returns the non-null GetCredentialRequest when the type of the request is {@link
+     * #TYPE_GET}, or null otherwise.
+     */
+    @Nullable
+    public GetCredentialRequest getGetCredentialRequest() {
+        return mGetCredentialRequest;
+    }
+
+    private RequestInfo(@NonNull IBinder token, @NonNull @RequestType String type,
+            boolean isFirstUsage, @NonNull String appDisplayName,
+            @Nullable CreateCredentialRequest createCredentialRequest,
+            @Nullable GetCredentialRequest getCredentialRequest) {
+        mToken = token;
+        mType = type;
+        mIsFirstUsage = isFirstUsage;
+        mAppDisplayName = appDisplayName;
+        mCreateCredentialRequest = createCredentialRequest;
+        mGetCredentialRequest = getCredentialRequest;
+    }
+
     protected RequestInfo(@NonNull Parcel in) {
         IBinder token = in.readStrongBinder();
         String type = in.readString8();
         boolean isFirstUsage = in.readBoolean();
+        String appDisplayName = in.readString8();
+        CreateCredentialRequest createCredentialRequest =
+                in.readTypedObject(CreateCredentialRequest.CREATOR);
+        GetCredentialRequest getCredentialRequest =
+                in.readTypedObject(GetCredentialRequest.CREATOR);
 
         mToken = token;
         AnnotationValidations.validate(NonNull.class, null, mToken);
         mType = type;
         AnnotationValidations.validate(NonNull.class, null, mType);
         mIsFirstUsage = isFirstUsage;
+        mAppDisplayName = appDisplayName;
+        AnnotationValidations.validate(NonNull.class, null, mAppDisplayName);
+        mCreateCredentialRequest = createCredentialRequest;
+        mGetCredentialRequest = getCredentialRequest;
     }
 
     @Override
@@ -95,6 +175,9 @@
         dest.writeStrongBinder(mToken);
         dest.writeString8(mType);
         dest.writeBoolean(mIsFirstUsage);
+        dest.writeString8(mAppDisplayName);
+        dest.writeTypedObject(mCreateCredentialRequest, flags);
+        dest.writeTypedObject(mGetCredentialRequest, flags);
     }
 
     @Override
diff --git a/core/java/android/credentials/ui/UserSelectionDialogResult.java b/core/java/android/credentials/ui/UserSelectionDialogResult.java
new file mode 100644
index 0000000..6025d78
--- /dev/null
+++ b/core/java/android/credentials/ui/UserSelectionDialogResult.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+/**
+ * Result data matching {@link BaseDialogResult#RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION}.
+ *
+ * @hide
+ */
+public class UserSelectionDialogResult extends BaseDialogResult implements Parcelable {
+    /** Parses and returns a UserSelectionDialogResult from the given resultData. */
+    @Nullable
+    public static UserSelectionDialogResult fromResultData(@NonNull Bundle resultData) {
+        return resultData.getParcelable(
+            EXTRA_USER_SELECTION_RESULT, UserSelectionDialogResult.class);
+    }
+
+    /**
+     * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+     *  ResultReceiver}.
+     */
+    public static void addToBundle(
+            @NonNull UserSelectionDialogResult result, @NonNull Bundle bundle) {
+        bundle.putParcelable(EXTRA_USER_SELECTION_RESULT, result);
+    }
+
+    /**
+     * The intent extra key for the {@code UserSelectionDialogResult} object when the credential
+     * selector activity finishes.
+     */
+    private static final String EXTRA_USER_SELECTION_RESULT =
+            "android.credentials.ui.extra.USER_SELECTION_RESULT";
+
+    @NonNull private final String mProviderId;
+    @NonNull private final String mEntryKey;
+    @NonNull private final String mEntrySubkey;
+
+    public UserSelectionDialogResult(
+            @NonNull IBinder requestToken, @NonNull String providerId,
+            @NonNull String entryKey, @NonNull String entrySubkey) {
+        super(requestToken);
+        mProviderId = providerId;
+        mEntryKey = entryKey;
+        mEntrySubkey = entrySubkey;
+    }
+
+    /** Returns provider package name whose entry was selected by the user. */
+    @NonNull
+    public String getProviderId() {
+        return mProviderId;
+    }
+
+    /** Returns the key of the visual entry that the user selected. */
+    @NonNull
+    public String getEntryKey() {
+        return mEntryKey;
+    }
+
+    /** Returns the subkey of the visual entry that the user selected. */
+    @NonNull
+    public String getEntrySubkey() {
+        return mEntrySubkey;
+    }
+
+    protected UserSelectionDialogResult(@NonNull Parcel in) {
+        super(in);
+        String providerId = in.readString8();
+        String entryKey = in.readString8();
+        String entrySubkey = in.readString8();
+
+        mProviderId = providerId;
+        AnnotationValidations.validate(NonNull.class, null, mProviderId);
+        mEntryKey = entryKey;
+        AnnotationValidations.validate(NonNull.class, null, mEntryKey);
+        mEntrySubkey = entrySubkey;
+        AnnotationValidations.validate(NonNull.class, null, mEntrySubkey);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString8(mProviderId);
+        dest.writeString8(mEntryKey);
+        dest.writeString8(mEntrySubkey);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @NonNull Creator<UserSelectionDialogResult> CREATOR =
+            new Creator<UserSelectionDialogResult>() {
+        @Override
+        public UserSelectionDialogResult createFromParcel(@NonNull Parcel in) {
+            return new UserSelectionDialogResult(in);
+        }
+
+        @Override
+        public UserSelectionDialogResult[] newArray(int size) {
+            return new UserSelectionDialogResult[size];
+        }
+    };
+}
diff --git a/core/java/android/credentials/ui/UserSelectionResult.java b/core/java/android/credentials/ui/UserSelectionResult.java
deleted file mode 100644
index 2ac5593..0000000
--- a/core/java/android/credentials/ui/UserSelectionResult.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.credentials.ui;
-
-import android.annotation.NonNull;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.AnnotationValidations;
-
-/**
- * User selection result information of a UX flow.
- *
- * Returned as part of the activity result intent data when the user dialog completes
- * successfully.
- *
- * @hide
- */
-public class UserSelectionResult implements Parcelable {
-
-    /**
-    * The intent extra key for the {@code UserSelectionResult} object when the credential selector
-    * activity finishes.
-    */
-    public static final String EXTRA_USER_SELECTION_RESULT =
-            "android.credentials.ui.extra.USER_SELECTION_RESULT";
-
-    @NonNull
-    private final IBinder mRequestToken;
-
-    @NonNull
-    private final String mProviderId;
-
-    // TODO: consider switching to string or other types, depending on the service implementation.
-    private final int mEntryId;
-
-    public UserSelectionResult(@NonNull IBinder requestToken, @NonNull String providerId,
-            int entryId) {
-        mRequestToken = requestToken;
-        mProviderId = providerId;
-        mEntryId = entryId;
-    }
-
-    /** Returns token of the app request that initiated this user dialog. */
-    @NonNull
-    public IBinder getRequestToken() {
-        return mRequestToken;
-    }
-
-    /** Returns provider package name whose entry was selected by the user. */
-    @NonNull
-    public String getProviderId() {
-        return mProviderId;
-    }
-
-    /** Returns the id of the visual entry that the user selected. */
-    public int getEntryId() {
-        return mEntryId;
-    }
-
-    protected UserSelectionResult(@NonNull Parcel in) {
-        IBinder requestToken = in.readStrongBinder();
-        String providerId = in.readString8();
-        int entryId = in.readInt();
-
-        mRequestToken = requestToken;
-        AnnotationValidations.validate(NonNull.class, null, mRequestToken);
-        mProviderId = providerId;
-        AnnotationValidations.validate(NonNull.class, null, mProviderId);
-        mEntryId = entryId;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeStrongBinder(mRequestToken);
-        dest.writeString8(mProviderId);
-        dest.writeInt(mEntryId);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    public static final @NonNull Creator<UserSelectionResult> CREATOR =
-            new Creator<UserSelectionResult>() {
-        @Override
-        public UserSelectionResult createFromParcel(@NonNull Parcel in) {
-            return new UserSelectionResult(in);
-        }
-
-        @Override
-        public UserSelectionResult[] newArray(int size) {
-            return new UserSelectionResult[size];
-        }
-    };
-}
diff --git a/core/java/android/graphics/fonts/FontUpdateRequest.java b/core/java/android/graphics/fonts/FontUpdateRequest.java
index dae09f0..510985c 100644
--- a/core/java/android/graphics/fonts/FontUpdateRequest.java
+++ b/core/java/android/graphics/fonts/FontUpdateRequest.java
@@ -24,7 +24,8 @@
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
 import android.text.FontConfig;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/hardware/CameraStreamStats.java b/core/java/android/hardware/CameraStreamStats.java
index 3952467..aed5a12 100644
--- a/core/java/android/hardware/CameraStreamStats.java
+++ b/core/java/android/hardware/CameraStreamStats.java
@@ -16,6 +16,7 @@
 package android.hardware;
 
 import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.params.ColorSpaceProfiles;
 import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -50,6 +51,7 @@
     private long[] mHistogramCounts;
     private long mDynamicRangeProfile;
     private long mStreamUseCase;
+    private int mColorSpace;
 
     private static final String TAG = "CameraStreamStats";
 
@@ -68,12 +70,13 @@
         mHistogramType = HISTOGRAM_TYPE_UNKNOWN;
         mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
         mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
     }
 
     public CameraStreamStats(int width, int height, int format, float maxPreviewFps,
             int dataSpace, long usage, long requestCount, long errorCount,
             int startLatencyMs, int maxHalBuffers, int maxAppBuffers, long dynamicRangeProfile,
-            long streamUseCase) {
+            long streamUseCase, int colorSpace) {
         mWidth = width;
         mHeight = height;
         mFormat = format;
@@ -88,6 +91,7 @@
         mHistogramType = HISTOGRAM_TYPE_UNKNOWN;
         mDynamicRangeProfile = dynamicRangeProfile;
         mStreamUseCase = streamUseCase;
+        mColorSpace = colorSpace;
     }
 
     public static final @android.annotation.NonNull Parcelable.Creator<CameraStreamStats> CREATOR =
@@ -136,6 +140,7 @@
         dest.writeLongArray(mHistogramCounts);
         dest.writeLong(mDynamicRangeProfile);
         dest.writeLong(mStreamUseCase);
+        dest.writeInt(mColorSpace);
     }
 
     public void readFromParcel(Parcel in) {
@@ -155,6 +160,7 @@
         mHistogramCounts = in.createLongArray();
         mDynamicRangeProfile = in.readLong();
         mStreamUseCase = in.readLong();
+        mColorSpace = in.readInt();
     }
 
     public int getWidth() {
@@ -217,6 +223,10 @@
         return mDynamicRangeProfile;
     }
 
+    public int getColorSpace() {
+        return mColorSpace;
+    }
+
     public long getStreamUseCase() {
         return mStreamUseCase;
     }
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 8873807..f634726 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -2224,6 +2224,7 @@
      *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING REMOSAIC_REPROCESSING}</li>
      *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT DYNAMIC_RANGE_TEN_BIT}</li>
      *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE STREAM_USE_CASE}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES COLOR_SPACE_PROFILES}</li>
      * </ul>
      *
      * <p>This key is available on all devices.</p>
@@ -2249,6 +2250,7 @@
      * @see #REQUEST_AVAILABLE_CAPABILITIES_REMOSAIC_REPROCESSING
      * @see #REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT
      * @see #REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE
+     * @see #REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES
      */
     @PublicKey
     @NonNull
@@ -2473,6 +2475,82 @@
             new Key<Long>("android.request.recommendedTenBitDynamicRangeProfile", long.class);
 
     /**
+     * <p>An interface for querying the color space profiles supported by a camera device.</p>
+     * <p>A color space profile is a combination of a color space, an image format, and a dynamic
+     * range profile. Camera clients can retrieve the list of supported color spaces by calling
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedColorSpaces } or
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedColorSpacesForDynamicRange }.
+     * If a camera does not support the
+     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT }
+     * capability, the dynamic range profile will always be
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD }. Color space
+     * capabilities are queried in combination with an {@link android.graphics.ImageFormat }.
+     * If a camera client wants to know the general color space capabilities of a camera device
+     * regardless of image format, it can specify {@link android.graphics.ImageFormat#UNKNOWN }.
+     * The color space for a session can be configured by setting the SessionConfiguration
+     * color space via {@link android.hardware.camera2.params.SessionConfiguration#setColorSpace }.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     */
+    @PublicKey
+    @NonNull
+    @SyntheticKey
+    public static final Key<android.hardware.camera2.params.ColorSpaceProfiles> REQUEST_AVAILABLE_COLOR_SPACE_PROFILES =
+            new Key<android.hardware.camera2.params.ColorSpaceProfiles>("android.request.availableColorSpaceProfiles", android.hardware.camera2.params.ColorSpaceProfiles.class);
+
+    /**
+     * <p>A list of all possible color space profiles supported by a camera device.</p>
+     * <p>A color space profile is a combination of a color space, an image format, and a dynamic range
+     * profile. If a camera does not support the
+     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT }
+     * capability, the dynamic range profile will always be
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD }. Camera clients can
+     * use {@link android.hardware.camera2.params.SessionConfiguration#setColorSpace } to select
+     * a color space.</p>
+     * <p><b>Possible values:</b></p>
+     * <ul>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED UNSPECIFIED}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SRGB SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_SRGB LINEAR_SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_EXTENDED_SRGB EXTENDED_SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_EXTENDED_SRGB LINEAR_EXTENDED_SRGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT709 BT709}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT2020 BT2020}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DCI_P3 DCI_P3}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DISPLAY_P3 DISPLAY_P3}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_NTSC_1953 NTSC_1953}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SMPTE_C SMPTE_C}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ADOBE_RGB ADOBE_RGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_PRO_PHOTO_RGB PRO_PHOTO_RGB}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACES ACES}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACESCG ACESCG}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_XYZ CIE_XYZ}</li>
+     *   <li>{@link #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_LAB CIE_LAB}</li>
+     * </ul>
+     *
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_EXTENDED_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_LINEAR_EXTENDED_SRGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT709
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_BT2020
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DCI_P3
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_DISPLAY_P3
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_NTSC_1953
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_SMPTE_C
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ADOBE_RGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_PRO_PHOTO_RGB
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACES
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_ACESCG
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_XYZ
+     * @see #REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_CIE_LAB
+     * @hide
+     */
+    public static final Key<long[]> REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP =
+            new Key<long[]>("android.request.availableColorSpaceProfilesMap", long[].class);
+
+    /**
      * <p>The list of image formats that are supported by this
      * camera device for output streams.</p>
      * <p>All camera devices will support JPEG and YUV_420_888 formats.</p>
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index dff2f7e..50551fee 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -133,9 +133,6 @@
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private FoldStateListener mFoldStateListener;
-    @GuardedBy("mLock")
-    private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>();
-    private boolean mFoldedDeviceState;
 
     /**
      * @hide
@@ -144,31 +141,39 @@
         void onDeviceStateChanged(boolean folded);
     }
 
-    private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+    private static final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
         private final int[] mFoldedDeviceStates;
 
+        private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners =
+                new ArrayList<>();
+        private boolean mFoldedDeviceState;
+
         public FoldStateListener(Context context) {
             mFoldedDeviceStates = context.getResources().getIntArray(
                     com.android.internal.R.array.config_foldedDeviceStates);
         }
 
-        private void handleStateChange(int state) {
+        private synchronized void handleStateChange(int state) {
             boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
-            synchronized (mLock) {
-                mFoldedDeviceState = folded;
-                ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
-                for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
-                    DeviceStateListener callback = listener.get();
-                    if (callback != null) {
-                        callback.onDeviceStateChanged(folded);
-                    } else {
-                        invalidListeners.add(listener);
-                    }
-                }
-                if (!invalidListeners.isEmpty()) {
-                    mDeviceStateListeners.removeAll(invalidListeners);
+
+            mFoldedDeviceState = folded;
+            ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
+            for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
+                DeviceStateListener callback = listener.get();
+                if (callback != null) {
+                    callback.onDeviceStateChanged(folded);
+                } else {
+                    invalidListeners.add(listener);
                 }
             }
+            if (!invalidListeners.isEmpty()) {
+                mDeviceStateListeners.removeAll(invalidListeners);
+            }
+        }
+
+        public synchronized void addDeviceStateListener(DeviceStateListener listener) {
+            listener.onDeviceStateChanged(mFoldedDeviceState);
+            mDeviceStateListeners.add(new WeakReference<>(listener));
         }
 
         @Override
@@ -192,9 +197,8 @@
     public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) {
         synchronized (mLock) {
             DeviceStateListener listener = chars.getDeviceStateListener();
-            listener.onDeviceStateChanged(mFoldedDeviceState);
             if (mFoldStateListener != null) {
-                mDeviceStateListeners.add(new WeakReference<>(listener));
+                mFoldStateListener.addDeviceStateListener(listener);
             }
         }
     }
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index c67a560..1e1d443 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -1257,6 +1257,24 @@
      */
     public static final int REQUEST_AVAILABLE_CAPABILITIES_STREAM_USE_CASE = 19;
 
+    /**
+     * <p>The device supports querying the possible combinations of color spaces, image
+     * formats, and dynamic range profiles supported by the camera and requesting a
+     * particular color space for a session via
+     * {@link android.hardware.camera2.params.SessionConfiguration#setColorSpace }.</p>
+     * <p>Cameras that enable this capability may or may not also implement dynamic range
+     * profiles. If they don't,
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedDynamicRangeProfiles }
+     * will return only
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD } and
+     * {@link android.hardware.camera2.params.ColorSpaceProfiles#getSupportedColorSpacesForDynamicRange }
+     * will assume support of the
+     * {@link android.hardware.camera2.params.DynamicRangeProfiles#STANDARD }
+     * profile in all combinations of color spaces and image formats.</p>
+     * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
+     */
+    public static final int REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES = 20;
+
     //
     // Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP
     //
@@ -1367,6 +1385,18 @@
     public static final int REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES_MAP_MAX = 0x1000;
 
     //
+    // Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP
+    //
+
+    /**
+     * <p>Default value, when not explicitly specified. The Camera device will choose the color
+     * space to employ.</p>
+     * @see CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP
+     * @hide
+     */
+    public static final int REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED = -1;
+
+    //
     // Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE
     //
 
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index ee12df5..012fad5 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -50,6 +50,7 @@
 import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfigurationDuration;
 import android.hardware.camera2.marshal.impl.MarshalQueryableString;
 import android.hardware.camera2.params.Capability;
+import android.hardware.camera2.params.ColorSpaceProfiles;
 import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.hardware.camera2.params.Face;
@@ -813,6 +814,15 @@
                     }
                 });
         sGetCommandMap.put(
+                CameraCharacteristics.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES.getNativeKey(),
+                        new GetCommand() {
+                    @Override
+                    @SuppressWarnings("unchecked")
+                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
+                        return (T) metadata.getColorSpaceProfiles();
+                    }
+                });
+        sGetCommandMap.put(
                 CaptureResult.STATISTICS_OIS_SAMPLES.getNativeKey(),
                         new GetCommand() {
                     @Override
@@ -1068,6 +1078,17 @@
         return new DynamicRangeProfiles(profileArray);
     }
 
+    private ColorSpaceProfiles getColorSpaceProfiles() {
+        long[] profileArray = getBase(
+                CameraCharacteristics.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP);
+
+        if (profileArray == null) {
+            return null;
+        }
+
+        return new ColorSpaceProfiles(profileArray);
+    }
+
     private Location getGpsLocation() {
         String processingMethod = get(CaptureResult.JPEG_GPS_PROCESSING_METHOD);
         double[] coords = get(CaptureResult.JPEG_GPS_COORDINATES);
diff --git a/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java
new file mode 100644
index 0000000..2e3af80
--- /dev/null
+++ b/core/java/android/hardware/camera2/params/ColorSpaceProfiles.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.camera2.params;
+
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.graphics.ColorSpace;
+import android.graphics.ImageFormat;
+import android.hardware.camera2.CameraMetadata;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Immutable class with information about supported color space profiles.
+ *
+ * <p>An instance of this class can be queried by retrieving the value of
+ * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}.
+ * </p>
+ *
+ * <p>All camera devices supporting the
+ * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_COLOR_SPACE_PROFILES}
+ * capability must advertise the supported color space profiles in
+ * {@link #getSupportedColorSpaces}</p>
+ *
+ * @see SessionConfiguration#setColorSpace
+ */
+public final class ColorSpaceProfiles {
+    /*
+     * @hide
+     */
+    public static final int UNSPECIFIED =
+            CameraMetadata.REQUEST_AVAILABLE_COLOR_SPACE_PROFILES_MAP_UNSPECIFIED;
+
+    private final Map<ColorSpace.Named, Map<Integer, Set<Long>>> mProfileMap = new ArrayMap<>();
+
+    /**
+     * Create a new immutable ColorSpaceProfiles instance.
+     *
+     * <p>This constructor takes over the array; do not write to the array afterwards.</p>
+     *
+     * <p>Do note that the constructor is available for testing purposes only!
+     * Camera clients must always retrieve the value of
+     * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}.
+     * for a given camera id in order to retrieve the device capabilities.</p>
+     *
+     * @param elements
+     *          An array of elements describing the map. It contains three elements per entry which
+     *          describe the supported color space profile value in the first element, a compatible
+     *          image format in the second, and in the third element a bitmap of compatible dynamic
+     *          range profiles (see {@link DynamicRangeProfiles#STANDARD} and others for the
+     *          individual bitmap components).
+     *
+     * @throws IllegalArgumentException
+     *            if the {@code elements} array length is invalid, not divisible by 3 or contains
+     *            invalid element values
+     * @throws NullPointerException
+     *            if {@code elements} is {@code null}
+     *
+     */
+    public ColorSpaceProfiles(@NonNull final long[] elements) {
+        if ((elements.length % 3) != 0) {
+            throw new IllegalArgumentException("Color space profile map length "
+                    + elements.length + " is not divisible by 3!");
+        }
+
+        for (int i = 0; i < elements.length; i += 3) {
+            int colorSpace = (int) elements[i];
+            checkProfileValue(colorSpace);
+            ColorSpace.Named namedColorSpace = ColorSpace.Named.values()[colorSpace];
+            int imageFormat = (int) elements[i + 1];
+            long dynamicRangeProfileBitmap = elements[i + 2];
+
+            if (!mProfileMap.containsKey(namedColorSpace)) {
+                ArrayMap<Integer, Set<Long>> imageFormatMap = new ArrayMap<>();
+                mProfileMap.put(namedColorSpace, imageFormatMap);
+            }
+
+            if (!mProfileMap.get(namedColorSpace).containsKey(imageFormat)) {
+                ArraySet<Long> dynamicRangeProfiles = new ArraySet<>();
+                mProfileMap.get(namedColorSpace).put(imageFormat, dynamicRangeProfiles);
+            }
+
+            if (dynamicRangeProfileBitmap != 0) {
+                for (long dynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+                        dynamicRangeProfile < DynamicRangeProfiles.PUBLIC_MAX;
+                        dynamicRangeProfile <<= 1) {
+                    if ((dynamicRangeProfileBitmap & dynamicRangeProfile) != 0) {
+                        mProfileMap.get(namedColorSpace).get(imageFormat).add(dynamicRangeProfile);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static void checkProfileValue(int colorSpace) {
+        boolean found = false;
+        for (ColorSpace.Named value : ColorSpace.Named.values()) {
+            if (colorSpace == value.ordinal()) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            throw new IllegalArgumentException("Unknown ColorSpace " + colorSpace);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public @NonNull Map<ColorSpace.Named, Map<Integer, Set<Long>>> getProfileMap() {
+        return mProfileMap;
+    }
+
+    /**
+     * Return a list of color spaces that are compatible with an ImageFormat. If ImageFormat.UNKNOWN
+     * is provided, this function will return a set of all unique color spaces supported by the
+     * device, regardless of image format.
+     *
+     * Color spaces which are compatible with ImageFormat.PRIVATE are able to be used with
+     * SurfaceView, SurfaceTexture, MediaCodec and MediaRecorder.
+     *
+     * @return set of color spaces
+     * @see SessionConfiguration#setColorSpace
+     * @see ColorSpace.Named
+     */
+    public @NonNull Set<ColorSpace.Named> getSupportedColorSpaces(
+            @ImageFormat.Format int imageFormat) {
+        ArraySet<ColorSpace.Named> supportedColorSpaceProfiles = new ArraySet<>();
+        for (ColorSpace.Named colorSpace : mProfileMap.keySet()) {
+            if (imageFormat == ImageFormat.UNKNOWN) {
+                supportedColorSpaceProfiles.add(colorSpace);
+            } else {
+                Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+                if (imageFormatMap.containsKey(imageFormat)) {
+                    supportedColorSpaceProfiles.add(colorSpace);
+                }
+            }
+        }
+        return supportedColorSpaceProfiles;
+    }
+
+    /**
+     * Return a list of image formats that are compatible with a color space.
+     *
+     * Color spaces which are compatible with ImageFormat.PRIVATE are able to be used with
+     * SurfaceView, SurfaceTexture, MediaCodec and MediaRecorder.
+     *
+     * @return set of image formats
+     * @see SessionConfiguration#setColorSpace
+     * @see ColorSpace.Named
+     */
+    public @NonNull Set<Integer> getSupportedImageFormatsForColorSpace(
+            @NonNull ColorSpace.Named colorSpace) {
+        Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+        if (imageFormatMap == null) {
+            return new ArraySet<Integer>();
+        }
+
+        return imageFormatMap.keySet();
+    }
+
+    /**
+     * Return a list of dynamic range profiles that are compatible with a color space and
+     * ImageFormat. If ImageFormat.UNKNOWN is provided, this function will return a set of
+     * all unique dynamic range profiles supported by the device given a color space,
+     * regardless of image format.
+     *
+     * @return set of dynamic range profiles.
+     * @see OutputConfiguration#setDynamicRangeProfile
+     * @see SessionConfiguration#setColorSpace
+     * @see ColorSpace.Named
+     * @see DynamicRangeProfiles.Profile
+     */
+    public @NonNull Set<Long> getSupportedDynamicRangeProfiles(@NonNull ColorSpace.Named colorSpace,
+            @ImageFormat.Format int imageFormat) {
+        Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+        if (imageFormatMap == null) {
+            return new ArraySet<Long>();
+        }
+
+        Set<Long> dynamicRangeProfiles = null;
+        if (imageFormat == ImageFormat.UNKNOWN) {
+            dynamicRangeProfiles = new ArraySet<>();
+            for (int supportedImageFormat : imageFormatMap.keySet()) {
+                Set<Long> supportedDynamicRangeProfiles = imageFormatMap.get(
+                        supportedImageFormat);
+                for (Long supportedDynamicRangeProfile : supportedDynamicRangeProfiles) {
+                    dynamicRangeProfiles.add(supportedDynamicRangeProfile);
+                }
+            }
+        } else {
+            dynamicRangeProfiles = imageFormatMap.get(imageFormat);
+            if (dynamicRangeProfiles == null) {
+                return new ArraySet<>();
+            }
+        }
+
+        return dynamicRangeProfiles;
+    }
+
+    /**
+     * Return a list of color spaces that are compatible with an ImageFormat and a dynamic range
+     * profile. If ImageFormat.UNKNOWN is provided, this function will return a set of all unique
+     * color spaces compatible with the given dynamic range profile, regardless of image format.
+     *
+     * @return set of color spaces
+     * @see SessionConfiguration#setColorSpace
+     * @see OutputConfiguration#setDynamicRangeProfile
+     * @see ColorSpace.Named
+     * @see DynamicRangeProfiles.Profile
+     */
+    public @NonNull Set<ColorSpace.Named> getSupportedColorSpacesForDynamicRange(
+            @ImageFormat.Format int imageFormat,
+            @DynamicRangeProfiles.Profile long dynamicRangeProfile) {
+        ArraySet<ColorSpace.Named> supportedColorSpaceProfiles = new ArraySet<>();
+        for (ColorSpace.Named colorSpace : mProfileMap.keySet()) {
+            Map<Integer, Set<Long>> imageFormatMap = mProfileMap.get(colorSpace);
+            if (imageFormat == ImageFormat.UNKNOWN) {
+                for (int supportedImageFormat : imageFormatMap.keySet()) {
+                    Set<Long> dynamicRangeProfiles = imageFormatMap.get(supportedImageFormat);
+                    if (dynamicRangeProfiles.contains(dynamicRangeProfile)) {
+                        supportedColorSpaceProfiles.add(colorSpace);
+                    }
+                }
+            } else if (imageFormatMap.containsKey(imageFormat)) {
+                Set<Long> dynamicRangeProfiles = imageFormatMap.get(imageFormat);
+                if (dynamicRangeProfiles.contains(dynamicRangeProfile)) {
+                    supportedColorSpaceProfiles.add(colorSpace);
+                }
+            }
+        }
+        return supportedColorSpaceProfiles;
+    }
+}
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index 90e92db..a0d8f8d 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -22,7 +22,10 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.graphics.ColorSpace;
 import android.graphics.ImageFormat;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
@@ -30,7 +33,6 @@
 import android.hardware.camera2.CameraMetadata;
 import android.hardware.camera2.MultiResolutionImageReader;
 import android.hardware.camera2.params.DynamicRangeProfiles;
-import android.hardware.camera2.params.DynamicRangeProfiles.Profile;
 import android.hardware.camera2.params.MultiResolutionStreamInfo;
 import android.hardware.camera2.utils.HashCodeHelpers;
 import android.hardware.camera2.utils.SurfaceUtils;
@@ -454,7 +456,7 @@
      * {@link android.media.MediaCodec} etc.)
      * or {@link ImageFormat#YCBCR_P010}.</p>
      */
-    public void setDynamicRangeProfile(@Profile long profile) {
+    public void setDynamicRangeProfile(@DynamicRangeProfiles.Profile long profile) {
         mDynamicRangeProfile = profile;
     }
 
@@ -463,11 +465,54 @@
      *
      * @return the currently set dynamic range profile
      */
-    public @Profile long getDynamicRangeProfile() {
+    public @DynamicRangeProfiles.Profile long getDynamicRangeProfile() {
         return mDynamicRangeProfile;
     }
 
     /**
+     * Set a specific device-supported color space.
+     *
+     * <p>Clients can choose from any profile advertised as supported in
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}
+     * queried using {@link ColorSpaceProfiles#getSupportedColorSpaces}.
+     * When set, the colorSpace will override the default color spaces of the output targets,
+     * or the color space implied by the dataSpace passed into an {@link ImageReader}'s
+     * constructor.</p>
+     *
+     * @hide
+     */
+    @TestApi
+    public void setColorSpace(@NonNull ColorSpace.Named colorSpace) {
+        mColorSpace = colorSpace.ordinal();
+    }
+
+    /**
+     * Clear the color space, such that the default color space will be used.
+     *
+     * @hide
+     */
+    @TestApi
+    public void clearColorSpace() {
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
+    }
+
+    /**
+     * Return the current color space.
+     *
+     * @return the currently set color space
+     * @hide
+     */
+    @TestApi
+    @SuppressLint("MethodNameUnits")
+    public @Nullable ColorSpace getColorSpace() {
+        if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) {
+            return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]);
+        } else {
+            return null;
+        }
+    }
+
+    /**
      * Create a new {@link OutputConfiguration} instance.
      *
      * <p>This constructor takes an argument for desired camera rotation</p>
@@ -530,6 +575,7 @@
         mIsMultiResolution = false;
         mSensorPixelModesUsed = new ArrayList<Integer>();
         mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
         mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
         mTimestampBase = TIMESTAMP_BASE_DEFAULT;
         mMirrorMode = MIRROR_MODE_AUTO;
@@ -631,6 +677,7 @@
         mIsMultiResolution = false;
         mSensorPixelModesUsed = new ArrayList<Integer>();
         mDynamicRangeProfile = DynamicRangeProfiles.STANDARD;
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
         mStreamUseCase = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT;
     }
 
@@ -1079,6 +1126,7 @@
         this.mIsMultiResolution = other.mIsMultiResolution;
         this.mSensorPixelModesUsed = other.mSensorPixelModesUsed;
         this.mDynamicRangeProfile = other.mDynamicRangeProfile;
+        this.mColorSpace = other.mColorSpace;
         this.mStreamUseCase = other.mStreamUseCase;
         this.mTimestampBase = other.mTimestampBase;
         this.mMirrorMode = other.mMirrorMode;
@@ -1105,6 +1153,7 @@
         checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
         long dynamicRangeProfile = source.readLong();
         DynamicRangeProfiles.checkProfileValue(dynamicRangeProfile);
+        int colorSpace = source.readInt();
 
         int timestampBase = source.readInt();
         int mirrorMode = source.readInt();
@@ -1132,6 +1181,7 @@
         mIsMultiResolution = isMultiResolutionOutput;
         mSensorPixelModesUsed = convertIntArrayToIntegerList(sensorPixelModesUsed);
         mDynamicRangeProfile = dynamicRangeProfile;
+        mColorSpace = colorSpace;
         mStreamUseCase = streamUseCase;
         mTimestampBase = timestampBase;
         mMirrorMode = mirrorMode;
@@ -1251,6 +1301,7 @@
         // writeList doesn't seem to work well with Integer list.
         dest.writeIntArray(convertIntegerToIntList(mSensorPixelModesUsed));
         dest.writeLong(mDynamicRangeProfile);
+        dest.writeInt(mColorSpace);
         dest.writeLong(mStreamUseCase);
         dest.writeInt(mTimestampBase);
         dest.writeInt(mMirrorMode);
@@ -1305,6 +1356,9 @@
             if (mDynamicRangeProfile != other.mDynamicRangeProfile) {
                 return false;
             }
+            if (mColorSpace != other.mColorSpace) {
+                return false;
+            }
 
             return true;
         }
@@ -1325,7 +1379,8 @@
                     mSurfaceGroupId, mSurfaceType, mIsShared ? 1 : 0,
                     mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode(),
                     mIsMultiResolution ? 1 : 0, mSensorPixelModesUsed.hashCode(),
-                    mDynamicRangeProfile, mStreamUseCase, mTimestampBase, mMirrorMode);
+                    mDynamicRangeProfile, mColorSpace, mStreamUseCase,
+                    mTimestampBase, mMirrorMode);
         }
 
         return HashCodeHelpers.hashCode(
@@ -1334,7 +1389,7 @@
                 mConfiguredDataspace, mSurfaceGroupId, mIsShared ? 1 : 0,
                 mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode(),
                 mIsMultiResolution ? 1 : 0, mSensorPixelModesUsed.hashCode(),
-                mDynamicRangeProfile, mStreamUseCase, mTimestampBase,
+                mDynamicRangeProfile, mColorSpace, mStreamUseCase, mTimestampBase,
                 mMirrorMode);
     }
 
@@ -1369,6 +1424,8 @@
     private ArrayList<Integer> mSensorPixelModesUsed;
     // Dynamic range profile
     private long mDynamicRangeProfile;
+    // Color space
+    private int mColorSpace;
     // Stream use case
     private long mStreamUseCase;
     // Timestamp base
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index cfb6efa..385f107 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -23,6 +23,8 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.graphics.ColorSpace;
 import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
@@ -30,6 +32,7 @@
 import android.hardware.camera2.params.InputConfiguration;
 import android.hardware.camera2.params.OutputConfiguration;
 import android.hardware.camera2.utils.HashCodeHelpers;
+import android.media.ImageReader;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
@@ -94,6 +97,7 @@
     private Executor mExecutor = null;
     private InputConfiguration mInputConfig = null;
     private CaptureRequest mSessionParameters = null;
+    private int mColorSpace;
 
     /**
      * Create a new {@link SessionConfiguration}.
@@ -314,4 +318,45 @@
     public CaptureRequest getSessionParameters() {
         return mSessionParameters;
     }
+
+    /**
+     * Set a specific device-supported color space.
+     *
+     * <p>Clients can choose from any profile advertised as supported in
+     * {@link CameraCharacteristics#REQUEST_AVAILABLE_COLOR_SPACE_PROFILES}
+     * queried using {@link ColorSpaceProfiles#getSupportedColorSpaces}.
+     * When set, the colorSpace will override the default color spaces of the output targets,
+     * or the color space implied by the dataSpace passed into an {@link ImageReader}'s
+     * constructor.</p>
+     */
+    public void setColorSpace(@NonNull ColorSpace.Named colorSpace) {
+        mColorSpace = colorSpace.ordinal();
+        for (OutputConfiguration outputConfiguration : mOutputConfigurations) {
+            outputConfiguration.setColorSpace(colorSpace);
+        }
+    }
+
+    /**
+     * Clear the color space, such that the default color space will be used.
+     */
+    public void clearColorSpace() {
+        mColorSpace = ColorSpaceProfiles.UNSPECIFIED;
+        for (OutputConfiguration outputConfiguration : mOutputConfigurations) {
+            outputConfiguration.clearColorSpace();
+        }
+    }
+
+    /**
+     * Return the current color space.
+     *
+     * @return the currently set color space
+     */
+    @SuppressLint("MethodNameUnits")
+    public @Nullable ColorSpace getColorSpace() {
+        if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) {
+            return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]);
+        } else {
+            return null;
+        }
+    }
 }
diff --git a/core/java/android/hardware/display/BrightnessConfiguration.java b/core/java/android/hardware/display/BrightnessConfiguration.java
index 366734e..007b37f 100644
--- a/core/java/android/hardware/display/BrightnessConfiguration.java
+++ b/core/java/android/hardware/display/BrightnessConfiguration.java
@@ -24,11 +24,11 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/hardware/display/BrightnessCorrection.java b/core/java/android/hardware/display/BrightnessCorrection.java
index 2919ec3..5ff08ba 100644
--- a/core/java/android/hardware/display/BrightnessCorrection.java
+++ b/core/java/android/hardware/display/BrightnessCorrection.java
@@ -23,10 +23,10 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.MathUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 00bccc6..6b3e673 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -24,11 +24,11 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.util.IntArray;
-import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.SurfaceControl;
+import android.view.SurfaceControl.RefreshRateRange;
 import android.view.SurfaceControl.Transaction;
 import android.window.DisplayWindowPolicyController;
 import android.window.ScreenCapture;
@@ -36,7 +36,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -651,72 +650,6 @@
     }
 
     /**
-     * Information about the min and max refresh rate DM would like to set the display to.
-     */
-    public static final class RefreshRateRange {
-        public static final String TAG = "RefreshRateRange";
-
-        // The tolerance within which we consider something approximately equals.
-        public static final float FLOAT_TOLERANCE = 0.01f;
-
-        /**
-         * The lowest desired refresh rate.
-         */
-        public float min;
-
-        /**
-         * The highest desired refresh rate.
-         */
-        public float max;
-
-        public RefreshRateRange() {}
-
-        public RefreshRateRange(float min, float max) {
-            if (min < 0 || max < 0 || min > max + FLOAT_TOLERANCE) {
-                Slog.e(TAG, "Wrong values for min and max when initializing RefreshRateRange : "
-                        + min + " " + max);
-                this.min = this.max = 0;
-                return;
-            }
-            if (min > max) {
-                // Min and max are within epsilon of each other, but in the wrong order.
-                float t = min;
-                min = max;
-                max = t;
-            }
-            this.min = min;
-            this.max = max;
-        }
-
-        /**
-         * Checks whether the two objects have the same values.
-         */
-        @Override
-        public boolean equals(Object other) {
-            if (other == this) {
-                return true;
-            }
-
-            if (!(other instanceof RefreshRateRange)) {
-                return false;
-            }
-
-            RefreshRateRange refreshRateRange = (RefreshRateRange) other;
-            return (min == refreshRateRange.min && max == refreshRateRange.max);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(min, max);
-        }
-
-        @Override
-        public String toString() {
-            return "(" + min + " " + max + ")";
-        }
-    }
-
-    /**
      * Describes a limitation on a display's refresh rate. Includes the allowed refresh rate
      * range as well as information about when it applies, such as high-brightness-mode.
      */
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 7247ef7..197739b 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -768,6 +768,20 @@
         }
     }
 
+    /**
+     * Schedules a watchdog.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void scheduleWatchdog() {
+        try {
+            mService.scheduleWatchdog();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private void cancelEnrollment(long requestId) {
         if (mService != null) {
             try {
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 9b56f43..2bf187a 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -172,4 +172,9 @@
 
     // Registers BiometricStateListener.
     void registerBiometricStateListener(IBiometricStateListener listener);
+
+    // Internal operation used to clear face biometric scheduler.
+    // Ensures that the scheduler is not stuck.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 0fd164d..5403f08 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -1080,7 +1080,7 @@
      */
     public boolean isPowerbuttonFps() {
         final FingerprintSensorPropertiesInternal sensorProps = getFirstFingerprintSensor();
-        return sensorProps.sensorType == TYPE_POWER_BUTTON;
+        return sensorProps == null ? false : sensorProps.sensorType == TYPE_POWER_BUTTON;
     }
 
     /**
@@ -1125,6 +1125,20 @@
     }
 
     /**
+     * Schedules a watchdog.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void scheduleWatchdog() {
+        try {
+            mService.scheduleWatchdog();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @hide
      */
     public void addLockoutResetCallback(final LockoutResetCallback callback) {
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index 1ba9a04..051e3a4 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -208,4 +208,9 @@
     // Sends a power button pressed event to all listeners.
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     oneway void onPowerPressed();
+
+    // Internal operation used to clear fingerprint biometric scheduler.
+    // Ensures that the scheduler is not stuck.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+    void scheduleWatchdog();
 }
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
index 2a79ef0..772ba8e 100644
--- a/core/java/android/hardware/input/VirtualInputDevice.java
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -49,6 +49,17 @@
         mToken = token;
     }
 
+    /**
+     * @return The device id of this device.
+     * @hide
+     */
+    public int getInputDeviceId() {
+        try {
+            return mVirtualDevice.getInputDeviceId(mToken);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 
     @Override
     @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java
index 8febed3..3ba3ebc 100644
--- a/core/java/android/hardware/radio/Announcement.java
+++ b/core/java/android/hardware/radio/Announcement.java
@@ -85,9 +85,9 @@
     /** @hide */
     public Announcement(@NonNull ProgramSelector selector, @Type int type,
             @NonNull Map<String, String> vendorInfo) {
-        mSelector = Objects.requireNonNull(selector);
-        mType = Objects.requireNonNull(type);
-        mVendorInfo = Objects.requireNonNull(vendorInfo);
+        mSelector = Objects.requireNonNull(selector, "Program selector cannot be null");
+        mType = type;
+        mVendorInfo = Objects.requireNonNull(vendorInfo, "Vendor info cannot be null");
     }
 
     private Announcement(@NonNull Parcel in) {
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index f2525d1..ade9fd6 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -160,6 +160,7 @@
      * Disables list updates and releases all resources.
      */
     public void close() {
+        OnCloseListener onCompleteListenersCopied = null;
         synchronized (mLock) {
             if (mIsClosed) return;
             mIsClosed = true;
@@ -167,10 +168,14 @@
             mListCallbacks.clear();
             mOnCompleteListeners.clear();
             if (mOnCloseListener != null) {
-                mOnCloseListener.onClose();
+                onCompleteListenersCopied = mOnCloseListener;
                 mOnCloseListener = null;
             }
         }
+
+        if (onCompleteListenersCopied != null) {
+            onCompleteListenersCopied.onClose();
+        }
     }
 
     void apply(Chunk chunk) {
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 4334116..8b71092 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -36,6 +36,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -1851,9 +1852,17 @@
     /**
      * @hide
      */
-    public RadioManager(@NonNull Context context) throws ServiceNotFoundException {
+    public RadioManager(Context context) throws ServiceNotFoundException {
+        this(context, IRadioService.Stub.asInterface(ServiceManager.getServiceOrThrow(
+                Context.RADIO_SERVICE)));
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public RadioManager(Context context, IRadioService service) {
         mContext = context;
-        mService = IRadioService.Stub.asInterface(
-                ServiceManager.getServiceOrThrow(Context.RADIO_SERVICE));
+        mService = service;
     }
 }
diff --git a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index 891da24..4f09bee 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -777,7 +777,7 @@
     }
 
     /**
-     * Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
+     * Invokes {@code IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
      * CharSequence, TextAttribute)}.
      *
      * @param start the character index where the replacement should start.
@@ -788,6 +788,8 @@
      *     that this means you can't position the cursor within the text.
      * @param text the text to replace. This may include styles.
      * @param textAttribute The extra information about the text. This value may be null.
+     * @return {@code true} if the invocation is completed without {@link RemoteException}, {@code
+     *     false} otherwise.
      */
     @AnyThread
     public boolean replaceText(
diff --git a/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java b/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java
index 76ee097..f8e2558 100644
--- a/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java
+++ b/core/java/android/net/netstats/NetworkStatsDataMigrationUtils.java
@@ -37,7 +37,7 @@
 import android.util.AtomicFile;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FastDataInput;
+import com.android.internal.util.ArtFastDataInput;
 
 import libcore.io.IoUtils;
 
@@ -254,7 +254,7 @@
     private static void readPlatformCollection(@NonNull NetworkStatsCollection.Builder builder,
             @NonNull File file) throws IOException {
         final FileInputStream is = new FileInputStream(file);
-        final FastDataInput dataIn = new FastDataInput(is, BUFFER_SIZE);
+        final ArtFastDataInput dataIn = new ArtFastDataInput(is, BUFFER_SIZE);
         try {
             readPlatformCollection(builder, dataIn);
         } finally {
diff --git a/core/java/android/os/AggregateBatteryConsumer.java b/core/java/android/os/AggregateBatteryConsumer.java
index 068df22..7a153ef 100644
--- a/core/java/android/os/AggregateBatteryConsumer.java
+++ b/core/java/android/os/AggregateBatteryConsumer.java
@@ -17,10 +17,11 @@
 package android.os;
 
 import android.annotation.NonNull;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 0c5f778..e2c52ce 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -22,12 +22,12 @@
 import android.database.CursorWindow;
 import android.util.Range;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.os.BatteryStatsHistory;
 import com.android.internal.os.BatteryStatsHistoryIterator;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index acfd15c..02704f5 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -22,12 +22,12 @@
 import android.annotation.Nullable;
 import android.util.ArrayMap;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index 522807b..5dffa0a 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -22,10 +22,11 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index d2d6bec..4a6772d 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -20,8 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java
index e1ec5cd..6b4a5cf 100644
--- a/core/java/android/os/UserBatteryConsumer.java
+++ b/core/java/android/os/UserBatteryConsumer.java
@@ -17,8 +17,9 @@
 package android.os;
 
 import android.annotation.NonNull;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/os/storage/OWNERS b/core/java/android/os/storage/OWNERS
index 1f686e5..c80c57c 100644
--- a/core/java/android/os/storage/OWNERS
+++ b/core/java/android/os/storage/OWNERS
@@ -1,11 +1,15 @@
 # Bug component: 95221
 
-corinac@google.com
-nandana@google.com
-zezeozue@google.com
-maco@google.com
-sahanas@google.com
+# Android Storage Team
 abkaur@google.com
-chiangi@google.com
-narayan@google.com
+corinac@google.com
 dipankarb@google.com
+krishang@google.com
+sahanas@google.com
+sergeynv@google.com
+shubhisaxena@google.com
+tylersaunders@google.com
+
+maco@google.com
+nandana@google.com
+narayan@google.com
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4e15b38..cd2bbeb 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6875,6 +6875,14 @@
         @Readable
         public static final String VOICE_INTERACTION_SERVICE = "voice_interaction_service";
 
+
+        /**
+         * The currently selected credential service(s) flattened ComponentName.
+         *
+         * @hide
+         */
+        public static final String CREDENTIAL_SERVICE = "credential_service";
+
         /**
          * The currently selected autofill service flattened ComponentName.
          * @hide
@@ -16036,6 +16044,18 @@
                 "key_chord_power_volume_up";
 
         /**
+         * Record audio from near-field microphone (ie. TV remote)
+         * Allows audio recording regardless of sensor privacy state,
+         * as it is an intentional user interaction: hold-to-talk
+         *
+         * Type: int (0 to disable, 1 to enable)
+         *
+         * @hide
+         */
+        public static final String RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED =
+                "receive_explicit_user_interaction_audio_enabled";
+
+        /**
          * Keyguard should be on the left hand side of the screen, for wide screen layouts.
          *
          * @hide
diff --git a/core/java/android/service/credentials/Action.java b/core/java/android/service/credentials/Action.java
index e2c11fb..553a324 100644
--- a/core/java/android/service/credentials/Action.java
+++ b/core/java/android/service/credentials/Action.java
@@ -50,9 +50,8 @@
     }
 
     private Action(@NonNull Parcel in) {
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
     }
 
     public static final @NonNull Creator<Action> CREATOR = new Creator<Action>() {
@@ -74,8 +73,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/CreateCredentialRequest.java b/core/java/android/service/credentials/CreateCredentialRequest.java
index 6a0bbc0..e6da349 100644
--- a/core/java/android/service/credentials/CreateCredentialRequest.java
+++ b/core/java/android/service/credentials/CreateCredentialRequest.java
@@ -54,7 +54,7 @@
     private CreateCredentialRequest(@NonNull Parcel in) {
         mCallingPackage = in.readString8();
         mType = in.readString8();
-        mData = in.readBundle();
+        mData = in.readTypedObject(Bundle.CREATOR);
     }
 
     public static final @NonNull Creator<CreateCredentialRequest> CREATOR =
@@ -79,7 +79,7 @@
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mCallingPackage);
         dest.writeString8(mType);
-        dest.writeBundle(mData);
+        dest.writeTypedObject(mData, flags);
     }
 
     /** Returns the calling package of the calling app. */
diff --git a/core/java/android/service/credentials/CreateCredentialResponse.java b/core/java/android/service/credentials/CreateCredentialResponse.java
index 613eba8..559b1ca 100644
--- a/core/java/android/service/credentials/CreateCredentialResponse.java
+++ b/core/java/android/service/credentials/CreateCredentialResponse.java
@@ -38,7 +38,9 @@
 
     private CreateCredentialResponse(@NonNull Parcel in) {
         mHeader = in.readCharSequence();
-        mSaveEntries = in.createTypedArrayList(SaveEntry.CREATOR);
+        List<SaveEntry> saveEntries = new ArrayList<>();
+        in.readTypedList(saveEntries, SaveEntry.CREATOR);
+        mSaveEntries = saveEntries;
     }
 
     @Override
diff --git a/core/java/android/service/credentials/CredentialEntry.java b/core/java/android/service/credentials/CredentialEntry.java
index 49b8435..4cc43a1 100644
--- a/core/java/android/service/credentials/CredentialEntry.java
+++ b/core/java/android/service/credentials/CredentialEntry.java
@@ -65,12 +65,10 @@
     }
 
     private CredentialEntry(@NonNull Parcel in) {
-        mType = in.readString();
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
-        mCredential = in.readParcelable(Credential.class.getClassLoader(),
-                Credential.class);
+        mType = in.readString8();
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+        mCredential = in.readTypedObject(Credential.CREATOR);
         mAutoSelectAllowed = in.readBoolean();
     }
 
@@ -95,9 +93,9 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeString8(mType);
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
-        mCredential.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
+        dest.writeTypedObject(mCredential, flags);
         dest.writeBoolean(mAutoSelectAllowed);
     }
 
diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java
new file mode 100644
index 0000000..e3f8cb7
--- /dev/null
+++ b/core/java/android/service/credentials/CredentialProviderInfo.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.credentials;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link ServiceInfo} and meta-data about a credential provider.
+ *
+ * @hide
+ */
+public final class CredentialProviderInfo {
+    private static final String TAG = "CredentialProviderInfo";
+
+    @NonNull
+    private final ServiceInfo mServiceInfo;
+    @NonNull
+    private final List<String> mCapabilities;
+
+    // TODO: Move the two strings below to CredentialProviderService when ready.
+    private static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
+    private static final String SERVICE_INTERFACE =
+            "android.service.credentials.CredentialProviderService";
+
+
+    /**
+     * Constructs an information instance of the credential provider.
+     *
+     * @param context The context object
+     * @param serviceComponent The serviceComponent of the provider service
+     * @param userId The android userId for which the current process is running
+     * @throws PackageManager.NameNotFoundException If provider service is not found
+     */
+    public CredentialProviderInfo(@NonNull Context context,
+            @NonNull ComponentName serviceComponent, int userId)
+            throws PackageManager.NameNotFoundException {
+        this(context, getServiceInfoOrThrow(serviceComponent, userId));
+    }
+
+    private CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) {
+        if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) {
+            Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName
+                    + "does not require permission"
+                    + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
+            throw new SecurityException("Service does not require the expected permission : "
+                    + Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE);
+        }
+        mServiceInfo = serviceInfo;
+        mCapabilities = new ArrayList<>();
+        populateProviderCapabilities(context);
+    }
+
+    private void populateProviderCapabilities(@NonNull Context context) {
+        if (mServiceInfo.applicationInfo.metaData == null) {
+            return;
+        }
+        try {
+            final int resourceId = mServiceInfo.applicationInfo.metaData.getInt(
+                    CAPABILITY_META_DATA_KEY);
+            String[] capabilities = context.getResources().getStringArray(resourceId);
+            if (capabilities == null) {
+                Log.w(TAG, "No capabilities found for provider: " + mServiceInfo.packageName);
+                return;
+            }
+            for (String capability : capabilities) {
+                if (capability.isEmpty()) {
+                    Log.w(TAG, "Skipping empty capability");
+                    continue;
+                }
+                mCapabilities.add(capability);
+            }
+        } catch (Resources.NotFoundException e) {
+            Log.w(TAG, "Exception while populating provider capabilities: " + e.getMessage());
+        }
+    }
+
+    private static ServiceInfo getServiceInfoOrThrow(@NonNull ComponentName serviceComponent,
+            int userId) throws PackageManager.NameNotFoundException {
+        try {
+            ServiceInfo si = AppGlobals.getPackageManager().getServiceInfo(
+                    serviceComponent,
+                    PackageManager.GET_META_DATA,
+                    userId);
+            if (si != null) {
+                return si;
+            }
+        } catch (RemoteException e) {
+            Slog.v(TAG, e.getMessage());
+        }
+        throw new PackageManager.NameNotFoundException(serviceComponent.toString());
+    }
+
+    /**
+     * Returns true if the service supports the given {@code credentialType}, false otherwise.
+     */
+    @NonNull
+    public boolean hasCapability(@NonNull String credentialType) {
+        return mCapabilities.contains(credentialType);
+    }
+
+    /** Returns the service info. */
+    @NonNull
+    public ServiceInfo getServiceInfo() {
+        return mServiceInfo;
+    }
+
+    /** Returns an immutable list of capabilities this provider service can support. */
+    @NonNull
+    public List<String> getCapabilities() {
+        return Collections.unmodifiableList(mCapabilities);
+    }
+
+    /**
+     * Returns the valid credential provider services available for the user with the
+     * given {@code userId}.
+     */
+    public static List<CredentialProviderInfo> getAvailableServices(@NonNull Context context,
+            @UserIdInt int userId) {
+        final List<CredentialProviderInfo> services = new ArrayList<>();
+
+        final List<ResolveInfo> resolveInfos =
+                context.getPackageManager().queryIntentServicesAsUser(
+                        new Intent(SERVICE_INTERFACE),
+                        PackageManager.GET_META_DATA,
+                        userId);
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            try {
+                services.add(new CredentialProviderInfo(context, serviceInfo));
+            } catch (SecurityException e) {
+                Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e);
+            }
+        }
+        return services;
+    }
+
+    /**
+     * Returns the valid credential provider services available for the user, that can
+     * support the given {@code credentialType}.
+     */
+    public static List<CredentialProviderInfo> getAvailableServicesForCapability(
+            Context context, @UserIdInt int userId, String credentialType) {
+        List<CredentialProviderInfo> servicesForCapability = new ArrayList<>();
+        final List<CredentialProviderInfo> services = getAvailableServices(context, userId);
+
+        for (CredentialProviderInfo service : services) {
+            if (service.hasCapability(credentialType)) {
+                servicesForCapability.add(service);
+            }
+        }
+        return servicesForCapability;
+    }
+}
diff --git a/core/java/android/service/credentials/CredentialsDisplayContent.java b/core/java/android/service/credentials/CredentialsDisplayContent.java
index 4133ea5..2cce169 100644
--- a/core/java/android/service/credentials/CredentialsDisplayContent.java
+++ b/core/java/android/service/credentials/CredentialsDisplayContent.java
@@ -53,8 +53,12 @@
 
     private CredentialsDisplayContent(@NonNull Parcel in) {
         mHeader = in.readCharSequence();
-        mCredentialEntries = in.createTypedArrayList(CredentialEntry.CREATOR);
-        mActions = in.createTypedArrayList(Action.CREATOR);
+        List<CredentialEntry> credentialEntries = new ArrayList<>();
+        in.readTypedList(credentialEntries, CredentialEntry.CREATOR);
+        mCredentialEntries = credentialEntries;
+        List<Action> actions = new ArrayList<>();
+        in.readTypedList(actions, Action.CREATOR);
+        mActions = actions;
     }
 
     public static final @NonNull Creator<CredentialsDisplayContent> CREATOR =
@@ -78,8 +82,8 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeCharSequence(mHeader);
-        dest.writeTypedList(mCredentialEntries);
-        dest.writeTypedList(mActions);
+        dest.writeTypedList(mCredentialEntries, flags);
+        dest.writeTypedList(mActions, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/GetCredentialsRequest.java b/core/java/android/service/credentials/GetCredentialsRequest.java
index 5b1a171..e06be44 100644
--- a/core/java/android/service/credentials/GetCredentialsRequest.java
+++ b/core/java/android/service/credentials/GetCredentialsRequest.java
@@ -49,8 +49,10 @@
     }
 
     private GetCredentialsRequest(@NonNull Parcel in) {
-        mCallingPackage = in.readString16NoHelper();
-        mGetCredentialOptions = in.createTypedArrayList(GetCredentialOption.CREATOR);
+        mCallingPackage = in.readString8();
+        List<GetCredentialOption> getCredentialOptions = new ArrayList<>();
+        in.readTypedList(getCredentialOptions, GetCredentialOption.CREATOR);
+        mGetCredentialOptions = getCredentialOptions;
     }
 
     public static final @NonNull Creator<GetCredentialsRequest> CREATOR =
@@ -73,7 +75,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeString16NoHelper(mCallingPackage);
+        dest.writeString8(mCallingPackage);
         dest.writeTypedList(mGetCredentialOptions);
     }
 
diff --git a/core/java/android/service/credentials/GetCredentialsResponse.java b/core/java/android/service/credentials/GetCredentialsResponse.java
index 980d9ae..979a699 100644
--- a/core/java/android/service/credentials/GetCredentialsResponse.java
+++ b/core/java/android/service/credentials/GetCredentialsResponse.java
@@ -78,9 +78,8 @@
     }
 
     private GetCredentialsResponse(@NonNull Parcel in) {
-        mCredentialsDisplayContent = in.readParcelable(CredentialsDisplayContent.class
-                .getClassLoader(), CredentialsDisplayContent.class);
-        mAuthenticationAction = in.readParcelable(Action.class.getClassLoader(), Action.class);
+        mCredentialsDisplayContent = in.readTypedObject(CredentialsDisplayContent.CREATOR);
+        mAuthenticationAction = in.readTypedObject(Action.CREATOR);
     }
 
     public static final @NonNull Creator<GetCredentialsResponse> CREATOR =
@@ -103,8 +102,8 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mCredentialsDisplayContent, flags);
-        dest.writeParcelable(mAuthenticationAction, flags);
+        dest.writeTypedObject(mCredentialsDisplayContent, flags);
+        dest.writeTypedObject(mAuthenticationAction, flags);
     }
 
     /**
diff --git a/core/java/android/service/credentials/SaveEntry.java b/core/java/android/service/credentials/SaveEntry.java
index 18644f0..abe51d4 100644
--- a/core/java/android/service/credentials/SaveEntry.java
+++ b/core/java/android/service/credentials/SaveEntry.java
@@ -40,10 +40,9 @@
     private final @Nullable Credential mCredential;
 
     private SaveEntry(@NonNull Parcel in) {
-        mSlice = in.readParcelable(Slice.class.getClassLoader(), Slice.class);
-        mPendingIntent = in.readParcelable(PendingIntent.class.getClassLoader(),
-                PendingIntent.class);
-        mCredential = in.readParcelable(Credential.class.getClassLoader(), Credential.class);
+        mSlice = in.readTypedObject(Slice.CREATOR);
+        mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
+        mCredential = in.readTypedObject(Credential.CREATOR);
     }
 
     public static final @NonNull Creator<SaveEntry> CREATOR = new Creator<SaveEntry>() {
@@ -65,9 +64,9 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        mSlice.writeToParcel(dest, flags);
-        mPendingIntent.writeToParcel(dest, flags);
-        mCredential.writeToParcel(dest, flags);
+        dest.writeTypedObject(mSlice, flags);
+        dest.writeTypedObject(mPendingIntent, flags);
+        dest.writeTypedObject(mCredential, flags);
     }
 
     /* package-private */ SaveEntry(
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index cb0dce9..32bdf79 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1047,7 +1047,7 @@
         }
 
         if (mDreamToken == null) {
-            Slog.w(mTag, "Finish was called before the dream was attached.");
+            if (mDebug) Slog.v(mTag, "finish() called when not attached.");
             stopSelf();
             return;
         }
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index bd4a495..cfc79e4 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -392,13 +392,13 @@
     public static final int NOTIFICATION_CHANNEL_OR_GROUP_DELETED = 3;
 
     /**
-     * An optional activity intent category that shows additional settings for what notifications
+     * An optional activity intent action that shows additional settings for what notifications
      * should be processed by this notification listener service. If defined, the OS may link to
      * this activity from the system notification listener service filter settings page.
      */
-    @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
-    public static final String INTENT_CATEGORY_SETTINGS_HOME =
-            "android.service.notification.category.SETTINGS_HOME";
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_SETTINGS_HOME =
+            "android.service.notification.action.SETTINGS_HOME";
 
     private final Object mLock = new Object();
 
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index e285b1c..eb9901a 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -48,12 +48,12 @@
 import android.util.ArraySet;
 import android.util.PluralsMessageFormatter;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/java/android/service/timezone/TimeZoneProviderEvent.java b/core/java/android/service/timezone/TimeZoneProviderEvent.java
index f6433b7..714afee 100644
--- a/core/java/android/service/timezone/TimeZoneProviderEvent.java
+++ b/core/java/android/service/timezone/TimeZoneProviderEvent.java
@@ -57,7 +57,7 @@
 
     /**
      * The provider was uncertain about the time zone. See {@link
-     * TimeZoneProviderService#reportUncertain()}
+     * TimeZoneProviderService#reportUncertain(TimeZoneProviderStatus)}
      */
     public static final @EventType int EVENT_TYPE_UNCERTAIN = 3;
 
@@ -66,42 +66,55 @@
     @ElapsedRealtimeLong
     private final long mCreationElapsedMillis;
 
+    // Populated when mType == EVENT_TYPE_SUGGESTION
     @Nullable
     private final TimeZoneProviderSuggestion mSuggestion;
 
+    // Populated when mType == EVENT_TYPE_PERMANENT_FAILURE
     @Nullable
     private final String mFailureCause;
 
-    private TimeZoneProviderEvent(@EventType int type,
+    // Populated when mType == EVENT_TYPE_SUGGESTION or EVENT_TYPE_UNCERTAIN
+    @Nullable
+    private final TimeZoneProviderStatus mTimeZoneProviderStatus;
+
+    private TimeZoneProviderEvent(int type,
             @ElapsedRealtimeLong long creationElapsedMillis,
             @Nullable TimeZoneProviderSuggestion suggestion,
-            @Nullable String failureCause) {
+            @Nullable String failureCause,
+            @Nullable TimeZoneProviderStatus timeZoneProviderStatus) {
         mType = type;
         mCreationElapsedMillis = creationElapsedMillis;
         mSuggestion = suggestion;
         mFailureCause = failureCause;
+        mTimeZoneProviderStatus = timeZoneProviderStatus;
     }
 
-    /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */
+    /** Returns an event of type {@link #EVENT_TYPE_SUGGESTION}. */
     public static TimeZoneProviderEvent createSuggestionEvent(
             @ElapsedRealtimeLong long creationElapsedMillis,
-            @NonNull TimeZoneProviderSuggestion suggestion) {
+            @NonNull TimeZoneProviderSuggestion suggestion,
+            @NonNull TimeZoneProviderStatus providerStatus) {
         return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION, creationElapsedMillis,
-                Objects.requireNonNull(suggestion), null);
+                Objects.requireNonNull(suggestion), null, Objects.requireNonNull(providerStatus));
     }
 
-    /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */
+    /** Returns an event of type {@link #EVENT_TYPE_UNCERTAIN}. */
     public static TimeZoneProviderEvent createUncertainEvent(
-            @ElapsedRealtimeLong long creationElapsedMillis) {
-        return new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null);
+            @ElapsedRealtimeLong long creationElapsedMillis,
+            @NonNull TimeZoneProviderStatus timeZoneProviderStatus) {
+
+        return new TimeZoneProviderEvent(
+                EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null,
+                Objects.requireNonNull(timeZoneProviderStatus));
     }
 
-    /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */
+    /** Returns an event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */
     public static TimeZoneProviderEvent createPermanentFailureEvent(
             @ElapsedRealtimeLong long creationElapsedMillis,
             @NonNull String cause) {
         return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, creationElapsedMillis, null,
-                Objects.requireNonNull(cause));
+                Objects.requireNonNull(cause), null);
     }
 
     /**
@@ -126,7 +139,7 @@
     }
 
     /**
-     * Returns the failure cauese. Populated when {@link #getType()} is {@link
+     * Returns the failure cause. Populated when {@link #getType()} is {@link
      * #EVENT_TYPE_PERMANENT_FAILURE}.
      */
     @Nullable
@@ -134,24 +147,34 @@
         return mFailureCause;
     }
 
-    public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR =
-            new Creator<TimeZoneProviderEvent>() {
-                @Override
-                public TimeZoneProviderEvent createFromParcel(Parcel in) {
-                    int type = in.readInt();
-                    long creationElapsedMillis = in.readLong();
-                    TimeZoneProviderSuggestion suggestion =
-                            in.readParcelable(getClass().getClassLoader(), android.service.timezone.TimeZoneProviderSuggestion.class);
-                    String failureCause = in.readString8();
-                    return new TimeZoneProviderEvent(
-                            type, creationElapsedMillis, suggestion, failureCause);
-                }
+    /**
+     * Returns the status of the time zone provider. Populated when {@link #getType()} is {@link
+     * #EVENT_TYPE_UNCERTAIN} or {@link #EVENT_TYPE_SUGGESTION}.
+     */
+    @Nullable
+    public TimeZoneProviderStatus getTimeZoneProviderStatus() {
+        return mTimeZoneProviderStatus;
+    }
 
-                @Override
-                public TimeZoneProviderEvent[] newArray(int size) {
-                    return new TimeZoneProviderEvent[size];
-                }
-            };
+    public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR = new Creator<>() {
+        @Override
+        public TimeZoneProviderEvent createFromParcel(Parcel in) {
+            int type = in.readInt();
+            long creationElapsedMillis = in.readLong();
+            TimeZoneProviderSuggestion suggestion = in.readParcelable(
+                    getClass().getClassLoader(), TimeZoneProviderSuggestion.class);
+            String failureCause = in.readString8();
+            TimeZoneProviderStatus status = in.readParcelable(
+                    getClass().getClassLoader(), TimeZoneProviderStatus.class);
+            return new TimeZoneProviderEvent(
+                    type, creationElapsedMillis, suggestion, failureCause, status);
+        }
+
+        @Override
+        public TimeZoneProviderEvent[] newArray(int size) {
+            return new TimeZoneProviderEvent[size];
+        }
+    };
 
     @Override
     public int describeContents() {
@@ -164,6 +187,7 @@
         parcel.writeLong(mCreationElapsedMillis);
         parcel.writeParcelable(mSuggestion, 0);
         parcel.writeString8(mFailureCause);
+        parcel.writeParcelable(mTimeZoneProviderStatus, 0);
     }
 
     @Override
@@ -173,14 +197,17 @@
                 + ", mCreationElapsedMillis=" + Duration.ofMillis(mCreationElapsedMillis).toString()
                 + ", mSuggestion=" + mSuggestion
                 + ", mFailureCause=" + mFailureCause
+                + ", mTimeZoneProviderStatus=" + mTimeZoneProviderStatus
                 + '}';
     }
 
     /**
      * Similar to {@link #equals} except this methods checks for equivalence, not equality.
-     * i.e. two {@link #EVENT_TYPE_UNCERTAIN} and {@link #EVENT_TYPE_PERMANENT_FAILURE} events are
-     * always equivalent, two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest
-     * the same time zones.
+     * i.e. two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest
+     * the same time zones and have the same provider status, two {@link #EVENT_TYPE_UNCERTAIN}
+     * events are equivalent if they have the same provider status, and {@link
+     * #EVENT_TYPE_PERMANENT_FAILURE} events are always equivalent (the nature of the failure is not
+     * considered).
      */
     @SuppressWarnings("ReferenceEquality")
     public boolean isEquivalentTo(@Nullable TimeZoneProviderEvent other) {
@@ -191,9 +218,10 @@
             return false;
         }
         if (mType == EVENT_TYPE_SUGGESTION) {
-            return mSuggestion.isEquivalentTo(other.getSuggestion());
+            return mSuggestion.isEquivalentTo(other.mSuggestion)
+                    && Objects.equals(mTimeZoneProviderStatus, other.mTimeZoneProviderStatus);
         }
-        return true;
+        return Objects.equals(mTimeZoneProviderStatus, other.mTimeZoneProviderStatus);
     }
 
     @Override
@@ -208,11 +236,13 @@
         return mType == that.mType
                 && mCreationElapsedMillis == that.mCreationElapsedMillis
                 && Objects.equals(mSuggestion, that.mSuggestion)
-                && Objects.equals(mFailureCause, that.mFailureCause);
+                && Objects.equals(mFailureCause, that.mFailureCause)
+                && Objects.equals(mTimeZoneProviderStatus, that.mTimeZoneProviderStatus);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause);
+        return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause,
+                mTimeZoneProviderStatus);
     }
 }
diff --git a/core/java/android/service/timezone/TimeZoneProviderService.java b/core/java/android/service/timezone/TimeZoneProviderService.java
index 0d215f6..cd4a305 100644
--- a/core/java/android/service/timezone/TimeZoneProviderService.java
+++ b/core/java/android/service/timezone/TimeZoneProviderService.java
@@ -203,6 +203,20 @@
      * details.
      */
     public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion) {
+        reportSuggestion(suggestion, TimeZoneProviderStatus.UNKNOWN);
+    }
+
+    /**
+     * Indicates a successful time zone detection. See {@link TimeZoneProviderSuggestion} for
+     * details.
+     *
+     * @param providerStatus provider status information that can influence detector service
+     *   behavior and/or be reported via the device UI
+     *
+     * @hide
+     */
+    public final void reportSuggestion(@NonNull TimeZoneProviderSuggestion suggestion,
+            @NonNull TimeZoneProviderStatus providerStatus) {
         Objects.requireNonNull(suggestion);
 
         mHandler.post(() -> {
@@ -212,7 +226,7 @@
                     try {
                         TimeZoneProviderEvent thisEvent =
                                 TimeZoneProviderEvent.createSuggestionEvent(
-                                        SystemClock.elapsedRealtime(), suggestion);
+                                        SystemClock.elapsedRealtime(), suggestion, providerStatus);
                         if (shouldSendEvent(thisEvent)) {
                             manager.onTimeZoneProviderEvent(thisEvent);
                             mLastEventSent = thisEvent;
@@ -231,6 +245,21 @@
      * to a time zone.
      */
     public final void reportUncertain() {
+        reportUncertain(TimeZoneProviderStatus.UNKNOWN);
+    }
+
+    /**
+     * Indicates the time zone is not known because of an expected runtime state or error.
+     *
+     * <p>When the status changes then a certain or uncertain report must be made to move the
+     * detector service to the new status.
+     *
+     * @param providerStatus provider status information that can influence detector service
+     *   behavior and/or be reported via the device UI
+     *
+     * @hide
+     */
+    public final void reportUncertain(@NonNull TimeZoneProviderStatus providerStatus) {
         mHandler.post(() -> {
             synchronized (mLock) {
                 ITimeZoneProviderManager manager = mManager;
@@ -238,7 +267,7 @@
                     try {
                         TimeZoneProviderEvent thisEvent =
                                 TimeZoneProviderEvent.createUncertainEvent(
-                                        SystemClock.elapsedRealtime());
+                                        SystemClock.elapsedRealtime(), providerStatus);
                         if (shouldSendEvent(thisEvent)) {
                             manager.onTimeZoneProviderEvent(thisEvent);
                             mLastEventSent = thisEvent;
diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.aidl b/core/java/android/service/timezone/TimeZoneProviderStatus.aidl
new file mode 100644
index 0000000..91dc7e9
--- /dev/null
+++ b/core/java/android/service/timezone/TimeZoneProviderStatus.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone;
+
+/**
+ * @hide
+ */
+parcelable TimeZoneProviderStatus;
diff --git a/core/java/android/service/timezone/TimeZoneProviderStatus.java b/core/java/android/service/timezone/TimeZoneProviderStatus.java
new file mode 100644
index 0000000..87d7843
--- /dev/null
+++ b/core/java/android/service/timezone/TimeZoneProviderStatus.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Objects;
+
+/**
+ * Information about the status of a {@link TimeZoneProviderService}.
+ *
+ * <p>Not all status properties or status values will apply to all provider implementations.
+ * {@code _NOT_APPLICABLE} status can be used to indicate properties that have no meaning for a
+ * given implementation.
+ *
+ * <p>Time zone providers are expected to work in one of two ways:
+ * <ol>
+ *     <li>Location: Providers will determine location and then map that location to one or more
+ *     time zone IDs.</li>
+ *     <li>External signals: Providers could use indirect signals like country code
+ *     and/or local offset / DST information provided to the device to infer a time zone, e.g.
+ *     signals like MCC and NITZ for telephony devices, IP geo location, or DHCP information
+ *     (RFC4833). The time zone ID could also be fed directly to the device by an external service.
+ *     </li>
+ * </ol>
+ *
+ * <p>The status properties are:
+ * <ul>
+ *     <li>location detection - for location-based providers, the status of the location detection
+ *     mechanism</li>
+ *     <li>connectivity - connectivity can influence providers directly, for example if they use
+ *     a networked service to map location to time zone ID, or use geo IP, or indirectly for
+ *     location detection (e.g. for the network location provider.</li>
+ *     <li>time zone resolution - the status related to determining a time zone ID or using a
+ *     detected time zone ID. For example, a networked service may be reachable (i.e. connectivity
+ *     is working) but the service could return errors, a time zone ID detected may not be usable
+ *     for a device because of TZDB version skew, or external indirect signals may available but
+ *     do not match the properties of a known time zone ID.</li>
+ * </ul>
+ *
+ * @hide
+ */
+public final class TimeZoneProviderStatus implements Parcelable {
+
+    /**
+     * A status code related to a dependency a provider may have.
+     *
+     * @hide
+     */
+    @IntDef(prefix = "DEPENDENCY_STATUS_", value = {
+            DEPENDENCY_STATUS_UNKNOWN,
+            DEPENDENCY_STATUS_NOT_APPLICABLE,
+            DEPENDENCY_STATUS_WORKING,
+            DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE,
+            DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT,
+            DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS,
+            DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DependencyStatus {}
+
+    /** The dependency's status is unknown. */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_UNKNOWN = 0;
+
+    /** The dependency is not used by the provider's implementation. */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_NOT_APPLICABLE = 1;
+
+    /** The dependency is applicable and working well. */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_WORKING = 2;
+
+    /**
+     * The dependency is used but is temporarily unavailable, e.g. connectivity has been lost for an
+     * unpredictable amount of time.
+     *
+     * <p>This status is considered normal is may be entered many times a day.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE = 3;
+
+    /**
+     * The dependency is used by the provider but is blocked by the environment in a way that the
+     * provider has detected and is considered likely to persist for some time, e.g. connectivity
+     * has been lost due to boarding a plane.
+     *
+     * <p>This status is considered unusual and could be used by the system as a trigger to try
+     * other time zone providers / time zone detection mechanisms. The bar for using this status
+     * should therefore be set fairly high to avoid a device bringing up other providers or
+     * switching to a different detection mechanism that may provide a different suggestion.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT = 4;
+
+    /**
+     * The dependency is used by the provider but is running in a degraded mode due to the user's
+     * settings. A user can take action to improve this, e.g. by changing a setting.
+     *
+     * <p>This status could be used by the system as a trigger to try other time zone
+     * providers / time zone detection mechanisms. The user may be informed.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_DEGRADED_BY_SETTINGS = 5;
+
+    /**
+     * The dependency is used by the provider but is completely blocked by the user's settings.
+     * A user can take action to correct this, e.g. by changing a setting.
+     *
+     * <p>This status could be used by the system as a trigger to try other time zone providers /
+     * time zone detection mechanisms. The user may be informed.
+     */
+    public static final @DependencyStatus int DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS = 6;
+
+    /**
+     * A status code related to an operation in a provider's detection algorithm.
+     *
+     * @hide
+     */
+    @IntDef(prefix = "OPERATION_STATUS_", value = {
+            OPERATION_STATUS_UNKNOWN,
+            OPERATION_STATUS_NOT_APPLICABLE,
+            OPERATION_STATUS_WORKING,
+            OPERATION_STATUS_FAILED,
+    })
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OperationStatus {}
+
+    /** The operation's status is unknown. */
+    public static final @OperationStatus int OPERATION_STATUS_UNKNOWN = 0;
+
+    /** The operation is not used by the provider's implementation. */
+    public static final @OperationStatus int OPERATION_STATUS_NOT_APPLICABLE = 1;
+
+    /** The operation is applicable and working well. */
+    public static final @OperationStatus int OPERATION_STATUS_WORKING = 2;
+
+    /** The operation is applicable and failed. */
+    public static final @OperationStatus int OPERATION_STATUS_FAILED = 3;
+
+    /**
+     * An instance that provides no information about status. Effectively a "null" status.
+     */
+    @NonNull
+    public static final TimeZoneProviderStatus UNKNOWN = new TimeZoneProviderStatus(
+            DEPENDENCY_STATUS_UNKNOWN, DEPENDENCY_STATUS_UNKNOWN, OPERATION_STATUS_UNKNOWN);
+
+    private final @DependencyStatus int mLocationDetectionStatus;
+    private final @DependencyStatus int mConnectivityStatus;
+    private final @OperationStatus int mTimeZoneResolutionStatus;
+
+    private TimeZoneProviderStatus(
+            @DependencyStatus int locationDetectionStatus,
+            @DependencyStatus int connectivityStatus,
+            @OperationStatus int timeZoneResolutionStatus) {
+        mLocationDetectionStatus = requireValidDependencyStatus(locationDetectionStatus);
+        mConnectivityStatus = requireValidDependencyStatus(connectivityStatus);
+        mTimeZoneResolutionStatus = requireValidOperationStatus(timeZoneResolutionStatus);
+    }
+
+    /**
+     * Returns the status of the location detection dependencies used by the provider (where
+     * applicable).
+     */
+    public @DependencyStatus int getLocationDetectionStatus() {
+        return mLocationDetectionStatus;
+    }
+
+    /**
+     * Returns the status of the connectivity dependencies used by the provider (where applicable).
+     */
+    public @DependencyStatus int getConnectivityStatus() {
+        return mConnectivityStatus;
+    }
+
+    /**
+     * Returns the status of the time zone resolution operation used by the provider.
+     */
+    public @OperationStatus int getTimeZoneResolutionStatus() {
+        return mTimeZoneResolutionStatus;
+    }
+
+    @Override
+    public String toString() {
+        return "TimeZoneProviderStatus{"
+                + "mLocationDetectionStatus=" + mLocationDetectionStatus
+                + ", mConnectivityStatus=" + mConnectivityStatus
+                + ", mTimeZoneResolutionStatus=" + mTimeZoneResolutionStatus
+                + '}';
+    }
+
+    public static final @NonNull Creator<TimeZoneProviderStatus> CREATOR = new Creator<>() {
+        @Override
+        public TimeZoneProviderStatus createFromParcel(Parcel in) {
+            @DependencyStatus int locationDetectionStatus = in.readInt();
+            @DependencyStatus int connectivityStatus = in.readInt();
+            @OperationStatus int timeZoneResolutionStatus = in.readInt();
+            return new TimeZoneProviderStatus(
+                    locationDetectionStatus, connectivityStatus, timeZoneResolutionStatus);
+        }
+
+        @Override
+        public TimeZoneProviderStatus[] newArray(int size) {
+            return new TimeZoneProviderStatus[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mLocationDetectionStatus);
+        parcel.writeInt(mConnectivityStatus);
+        parcel.writeInt(mTimeZoneResolutionStatus);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        TimeZoneProviderStatus that = (TimeZoneProviderStatus) o;
+        return mLocationDetectionStatus == that.mLocationDetectionStatus
+                && mConnectivityStatus == that.mConnectivityStatus
+                && mTimeZoneResolutionStatus == that.mTimeZoneResolutionStatus;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mLocationDetectionStatus, mConnectivityStatus, mTimeZoneResolutionStatus);
+    }
+
+    /** A builder for {@link TimeZoneProviderStatus}. */
+    public static final class Builder {
+
+        private @DependencyStatus int mLocationDetectionStatus = DEPENDENCY_STATUS_UNKNOWN;
+        private @DependencyStatus int mConnectivityStatus = DEPENDENCY_STATUS_UNKNOWN;
+        private @OperationStatus int mTimeZoneResolutionStatus = OPERATION_STATUS_UNKNOWN;
+
+        /**
+         * Creates a new builder instance. At creation time all status properties are set to
+         * their "UNKNOWN" value.
+         */
+        public Builder() {
+        }
+
+        /**
+         * @hide
+         */
+        public Builder(TimeZoneProviderStatus toCopy) {
+            mLocationDetectionStatus = toCopy.mLocationDetectionStatus;
+            mConnectivityStatus = toCopy.mConnectivityStatus;
+            mTimeZoneResolutionStatus = toCopy.mTimeZoneResolutionStatus;
+        }
+
+        /**
+         * Sets the status of the provider's location detection dependency (where applicable).
+         * See the {@code DEPENDENCY_STATUS_} constants for more information.
+         */
+        @NonNull
+        public Builder setLocationDetectionStatus(@DependencyStatus int locationDetectionStatus) {
+            mLocationDetectionStatus = locationDetectionStatus;
+            return this;
+        }
+
+        /**
+         * Sets the status of the provider's connectivity dependency (where applicable).
+         * See the {@code DEPENDENCY_STATUS_} constants for more information.
+         */
+        @NonNull
+        public Builder setConnectivityStatus(@DependencyStatus int connectivityStatus) {
+            mConnectivityStatus = connectivityStatus;
+            return this;
+        }
+
+        /**
+         * Sets the status of the provider's time zone resolution operation.
+         * See the {@code OPERATION_STATUS_} constants for more information.
+         */
+        @NonNull
+        public Builder setTimeZoneResolutionStatus(@OperationStatus int timeZoneResolutionStatus) {
+            mTimeZoneResolutionStatus = timeZoneResolutionStatus;
+            return this;
+        }
+
+        /**
+         * Builds a {@link TimeZoneProviderStatus} instance.
+         */
+        @NonNull
+        public TimeZoneProviderStatus build() {
+            return new TimeZoneProviderStatus(
+                    mLocationDetectionStatus, mConnectivityStatus, mTimeZoneResolutionStatus);
+        }
+    }
+
+    private @OperationStatus int requireValidOperationStatus(@OperationStatus int operationStatus) {
+        if (operationStatus < OPERATION_STATUS_UNKNOWN
+                || operationStatus > OPERATION_STATUS_FAILED) {
+            throw new IllegalArgumentException(Integer.toString(operationStatus));
+        }
+        return operationStatus;
+    }
+
+    private static @DependencyStatus int requireValidDependencyStatus(
+            @DependencyStatus int dependencyStatus) {
+        if (dependencyStatus < DEPENDENCY_STATUS_UNKNOWN
+                || dependencyStatus > DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS) {
+            throw new IllegalArgumentException(Integer.toString(dependencyStatus));
+        }
+        return dependencyStatus;
+    }
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 519fc55..1337d6a 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -36,7 +36,6 @@
 import android.text.style.ParagraphStyle;
 import android.text.style.ReplacementSpan;
 import android.text.style.TabStopSpan;
-import android.util.Range;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -1859,13 +1858,12 @@
      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
      *     text segment
      * @param inclusionStrategy strategy for determining whether a text segment is inside the
-     *          specified area
-     * @return an integer range where the endpoints are the start (inclusive) and end (exclusive)
-     *     character offsets of the text range, or null if there are no text segments inside the
-     *     area
+     *     specified area
+     * @return int array of size 2 containing the start (inclusive) and end (exclusive) character
+     *     offsets of the text range, or null if there are no text segments inside the area
      */
     @Nullable
-    public Range<Integer> getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
+    public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
             @NonNull TextInclusionStrategy inclusionStrategy) {
         // Find the first line whose bottom (without line spacing) is below the top of the area.
         int startLine = getLineForVertical((int) area.top);
@@ -1923,7 +1921,7 @@
         start = segmentFinder.previousStartBoundary(start + 1);
         end = segmentFinder.nextEndBoundary(end - 1);
 
-        return new Range(start, end);
+        return new int[] {start, end};
     }
 
     /**
diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java
index 3b08c3b..7c83087 100644
--- a/core/java/android/util/CharsetUtils.java
+++ b/core/java/android/util/CharsetUtils.java
@@ -18,6 +18,8 @@
 
 import android.annotation.NonNull;
 
+import com.android.modules.utils.ModifiedUtf8;
+
 import dalvik.annotation.optimization.FastNative;
 
 /**
@@ -30,8 +32,7 @@
  * Callers are cautioned that there is a long-standing ART bug that emits
  * non-standard 4-byte sequences, as described by {@code kUtfUse4ByteSequence}
  * in {@code art/runtime/jni/jni_internal.cc}. If precise modified UTF-8
- * encoding is required, use {@link com.android.internal.util.ModifiedUtf8}
- * instead.
+ * encoding is required, use {@link ModifiedUtf8} instead.
  *
  * @hide
  */
@@ -43,8 +44,8 @@
      * Callers are cautioned that there is a long-standing ART bug that emits
      * non-standard 4-byte sequences, as described by
      * {@code kUtfUse4ByteSequence} in {@code art/runtime/jni/jni_internal.cc}.
-     * If precise modified UTF-8 encoding is required, use
-     * {@link com.android.internal.util.ModifiedUtf8} instead.
+     * If precise modified UTF-8 encoding is required, use {@link ModifiedUtf8}
+     * instead.
      *
      * @param src string value to be encoded
      * @param dest destination byte array to encode into
@@ -66,8 +67,8 @@
      * Callers are cautioned that there is a long-standing ART bug that emits
      * non-standard 4-byte sequences, as described by
      * {@code kUtfUse4ByteSequence} in {@code art/runtime/jni/jni_internal.cc}.
-     * If precise modified UTF-8 encoding is required, use
-     * {@link com.android.internal.util.ModifiedUtf8} instead.
+     * If precise modified UTF-8 encoding is required, use {@link ModifiedUtf8}
+     * instead.
      *
      * @param src string value to be encoded
      * @param srcLen exact length of string to be encoded
@@ -88,8 +89,8 @@
      * Callers are cautioned that there is a long-standing ART bug that emits
      * non-standard 4-byte sequences, as described by
      * {@code kUtfUse4ByteSequence} in {@code art/runtime/jni/jni_internal.cc}.
-     * If precise modified UTF-8 encoding is required, use
-     * {@link com.android.internal.util.ModifiedUtf8} instead.
+     * If precise modified UTF-8 encoding is required, use {@link ModifiedUtf8}
+     * instead.
      *
      * @param src source byte array to decode from
      * @param srcOff offset into source where decoding should begin
diff --git a/core/java/android/util/TypedXmlPullParser.java b/core/java/android/util/TypedXmlPullParser.java
deleted file mode 100644
index aa68bf4..0000000
--- a/core/java/android/util/TypedXmlPullParser.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.util;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-/**
- * Specialization of {@link XmlPullParser} which adds explicit methods to
- * support consistent and efficient conversion of primitive data types.
- *
- * @hide
- */
-public interface TypedXmlPullParser extends XmlPullParser {
-    /**
-     * @return index of requested attribute, otherwise {@code -1} if undefined
-     */
-    default int getAttributeIndex(@Nullable String namespace, @NonNull String name) {
-        final boolean namespaceNull = (namespace == null);
-        final int count = getAttributeCount();
-        for (int i = 0; i < count; i++) {
-            if ((namespaceNull || namespace.equals(getAttributeNamespace(i)))
-                    && name.equals(getAttributeName(i))) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    /**
-     * @return index of requested attribute
-     * @throws XmlPullParserException if the value is undefined
-     */
-    default int getAttributeIndexOrThrow(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) {
-            throw new XmlPullParserException("Missing attribute " + name);
-        } else {
-            return index;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    @NonNull byte[] getAttributeBytesHex(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    @NonNull byte[] getAttributeBytesBase64(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    int getAttributeInt(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    int getAttributeIntHex(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    long getAttributeLong(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    long getAttributeLongHex(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    float getAttributeFloat(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    double getAttributeDouble(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed
-     */
-    boolean getAttributeBoolean(int index) throws XmlPullParserException;
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default @NonNull byte[] getAttributeBytesHex(@Nullable String namespace,
-            @NonNull String name) throws XmlPullParserException {
-        return getAttributeBytesHex(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default @NonNull byte[] getAttributeBytesBase64(@Nullable String namespace,
-            @NonNull String name) throws XmlPullParserException {
-        return getAttributeBytesBase64(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default int getAttributeInt(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeInt(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default int getAttributeIntHex(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeIntHex(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default long getAttributeLong(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeLong(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default long getAttributeLongHex(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeLongHex(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default float getAttributeFloat(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeFloat(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default double getAttributeDouble(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeDouble(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}
-     * @throws XmlPullParserException if the value is malformed or undefined
-     */
-    default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name)
-            throws XmlPullParserException {
-        return getAttributeBoolean(getAttributeIndexOrThrow(namespace, name));
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default @Nullable byte[] getAttributeBytesHex(@Nullable String namespace,
-            @NonNull String name, @Nullable byte[] defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeBytesHex(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default @Nullable byte[] getAttributeBytesBase64(@Nullable String namespace,
-            @NonNull String name, @Nullable byte[] defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeBytesBase64(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default int getAttributeInt(@Nullable String namespace, @NonNull String name,
-            int defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeInt(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default int getAttributeIntHex(@Nullable String namespace, @NonNull String name,
-            int defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeIntHex(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default long getAttributeLong(@Nullable String namespace, @NonNull String name,
-            long defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeLong(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default long getAttributeLongHex(@Nullable String namespace, @NonNull String name,
-            long defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeLongHex(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default float getAttributeFloat(@Nullable String namespace, @NonNull String name,
-            float defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeFloat(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default double getAttributeDouble(@Nullable String namespace, @NonNull String name,
-            double defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeDouble(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-
-    /**
-     * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
-     *         default value if the value is malformed or undefined
-     */
-    default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name,
-            boolean defaultValue) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index == -1) return defaultValue;
-        try {
-            return getAttributeBoolean(index);
-        } catch (Exception ignored) {
-            return defaultValue;
-        }
-    }
-}
diff --git a/core/java/android/util/TypedXmlSerializer.java b/core/java/android/util/TypedXmlSerializer.java
deleted file mode 100644
index 3f9eaa8..0000000
--- a/core/java/android/util/TypedXmlSerializer.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.util;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-
-/**
- * Specialization of {@link XmlSerializer} which adds explicit methods to
- * support consistent and efficient conversion of primitive data types.
- *
- * @hide
- */
-public interface TypedXmlSerializer extends XmlSerializer {
-    /**
-     * Functionally equivalent to {@link #attribute(String, String, String)} but
-     * with the additional signal that the given value is a candidate for being
-     * canonicalized, similar to {@link String#intern()}.
-     */
-    @NonNull XmlSerializer attributeInterned(@Nullable String namespace, @NonNull String name,
-            @NonNull String value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeBytesHex(@Nullable String namespace, @NonNull String name,
-            @NonNull byte[] value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeBytesBase64(@Nullable String namespace, @NonNull String name,
-            @NonNull byte[] value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeInt(@Nullable String namespace, @NonNull String name,
-            int value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeIntHex(@Nullable String namespace, @NonNull String name,
-            int value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeLong(@Nullable String namespace, @NonNull String name,
-            long value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeLongHex(@Nullable String namespace, @NonNull String name,
-            long value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeFloat(@Nullable String namespace, @NonNull String name,
-            float value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeDouble(@Nullable String namespace, @NonNull String name,
-            double value) throws IOException;
-
-    /**
-     * Encode the given strongly-typed value and serialize using
-     * {@link #attribute(String, String, String)}.
-     */
-    @NonNull XmlSerializer attributeBoolean(@Nullable String namespace, @NonNull String name,
-            boolean value) throws IOException;
-}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index 38decf9..33058d8 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -22,10 +22,13 @@
 import android.system.ErrnoException;
 import android.system.Os;
 
-import com.android.internal.util.BinaryXmlPullParser;
-import com.android.internal.util.BinaryXmlSerializer;
+import com.android.internal.util.ArtBinaryXmlPullParser;
+import com.android.internal.util.ArtBinaryXmlSerializer;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.BinaryXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.util.XmlObjectFactory;
 
@@ -146,7 +149,7 @@
      * @hide
      */
     public static @NonNull TypedXmlPullParser newBinaryPullParser() {
-        return new BinaryXmlPullParser();
+        return new ArtBinaryXmlPullParser();
     }
 
     /**
@@ -225,7 +228,7 @@
      * @hide
      */
     public static @NonNull TypedXmlSerializer newBinarySerializer() {
-        return new BinaryXmlSerializer();
+        return new ArtBinaryXmlSerializer();
     }
 
     /**
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 9789b56..06c1b25 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -2001,7 +2001,6 @@
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index c46f33a..06851de 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -67,6 +67,7 @@
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.Slog;
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 import android.view.Surface.OutOfResourcesException;
@@ -1673,6 +1674,146 @@
         return nativeGetDisplayedContentSample(displayToken, maxFrames, timestamp);
     }
 
+    /**
+     * Information about the min and max refresh rate DM would like to set the display to.
+     * @hide
+     */
+    public static final class RefreshRateRange {
+        public static final String TAG = "RefreshRateRange";
+
+        // The tolerance within which we consider something approximately equals.
+        public static final float FLOAT_TOLERANCE = 0.01f;
+
+        /**
+         * The lowest desired refresh rate.
+         */
+        public float min;
+
+        /**
+         * The highest desired refresh rate.
+         */
+        public float max;
+
+        public RefreshRateRange() {}
+
+        public RefreshRateRange(float min, float max) {
+            if (min < 0 || max < 0 || min > max + FLOAT_TOLERANCE) {
+                Slog.e(TAG, "Wrong values for min and max when initializing RefreshRateRange : "
+                        + min + " " + max);
+                this.min = this.max = 0;
+                return;
+            }
+            if (min > max) {
+                // Min and max are within epsilon of each other, but in the wrong order.
+                float t = min;
+                min = max;
+                max = t;
+            }
+            this.min = min;
+            this.max = max;
+        }
+
+        /**
+         * Checks whether the two objects have the same values.
+         */
+        @Override
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            }
+
+            if (!(other instanceof RefreshRateRange)) {
+                return false;
+            }
+
+            RefreshRateRange refreshRateRange = (RefreshRateRange) other;
+            return (min == refreshRateRange.min && max == refreshRateRange.max);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(min, max);
+        }
+
+        @Override
+        public String toString() {
+            return "(" + min + " " + max + ")";
+        }
+
+        /**
+         * Copies the supplied object's values to this object.
+         */
+        public void copyFrom(RefreshRateRange other) {
+            this.min = other.min;
+            this.max = other.max;
+        }
+    }
+
+    /**
+     * Information about the ranges of refresh rates for the display physical refresh rates and the
+     * render frame rate DM would like to set the policy to.
+     * @hide
+     */
+    public static final class RefreshRateRanges {
+        public static final String TAG = "RefreshRateRanges";
+
+        /**
+         *  The range of refresh rates that the display should run at.
+         */
+        public final RefreshRateRange physical;
+
+        /**
+         *  The range of refresh rates that apps should render at.
+         */
+        public final RefreshRateRange render;
+
+        public RefreshRateRanges() {
+            physical = new RefreshRateRange();
+            render = new RefreshRateRange();
+        }
+
+        public RefreshRateRanges(RefreshRateRange physical, RefreshRateRange render) {
+            this.physical = new RefreshRateRange(physical.min, physical.max);
+            this.render = new RefreshRateRange(render.min, render.max);
+        }
+
+        /**
+         * Checks whether the two objects have the same values.
+         */
+        @Override
+        public boolean equals(Object other) {
+            if (other == this) {
+                return true;
+            }
+
+            if (!(other instanceof RefreshRateRanges)) {
+                return false;
+            }
+
+            RefreshRateRanges rates = (RefreshRateRanges) other;
+            return physical.equals(rates.physical) && render.equals(
+                    rates.render);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(physical, render);
+        }
+
+        @Override
+        public String toString() {
+            return "physical: " + physical + " render:  " + render;
+        }
+
+        /**
+         * Copies the supplied object's values to this object.
+         */
+        public void copyFrom(RefreshRateRanges other) {
+            this.physical.copyFrom(other.physical);
+            this.render.copyFrom(other.render);
+        }
+    }
+
 
     /**
      * Contains information about desired display configuration.
@@ -1682,44 +1823,49 @@
     public static final class DesiredDisplayModeSpecs {
         public int defaultMode;
         /**
-         * The primary refresh rate range represents display manager's general guidance on the
-         * display configs surface flinger will consider when switching refresh rates. Unless
-         * surface flinger has a specific reason to do otherwise, it will stay within this range.
-         */
-        public float primaryRefreshRateMin;
-        public float primaryRefreshRateMax;
-        /**
-         * The app request refresh rate range allows surface flinger to consider more display
-         * configs when switching refresh rates. Although surface flinger will generally stay within
-         * the primary range, specific considerations, such as layer frame rate settings specified
-         * via the setFrameRate() api, may cause surface flinger to go outside the primary
-         * range. Surface flinger never goes outside the app request range. The app request range
-         * will be greater than or equal to the primary refresh rate range, never smaller.
-         */
-        public float appRequestRefreshRateMin;
-        public float appRequestRefreshRateMax;
-
-        /**
          * If true this will allow switching between modes in different display configuration
          * groups. This way the user may see visual interruptions when the display mode changes.
          */
         public boolean allowGroupSwitching;
 
-        public DesiredDisplayModeSpecs() {}
+        /**
+         * The primary physical and render refresh rate ranges represent display manager's general
+         * guidance on the display configs surface flinger will consider when switching refresh
+         * rates and scheduling the frame rate. Unless surface flinger has a specific reason to do
+         * otherwise, it will stay within this range.
+         */
+        public final RefreshRateRanges primaryRanges;
+
+        /**
+         * The app request physical and render refresh rate ranges allow surface flinger to consider
+         * more display configs when switching refresh rates. Although surface flinger will
+         * generally stay within the primary range, specific considerations, such as layer frame
+         * rate settings specified via the setFrameRate() api, may cause surface flinger to go
+         * outside the primary range. Surface flinger never goes outside the app request range.
+         * The app request range will be greater than or equal to the primary refresh rate range,
+         * never smaller.
+         */
+        public final RefreshRateRanges appRequestRanges;
+
+        public DesiredDisplayModeSpecs() {
+            this.primaryRanges = new RefreshRateRanges();
+            this.appRequestRanges = new RefreshRateRanges();
+        }
 
         public DesiredDisplayModeSpecs(DesiredDisplayModeSpecs other) {
+            this.primaryRanges = new RefreshRateRanges();
+            this.appRequestRanges = new RefreshRateRanges();
             copyFrom(other);
         }
 
         public DesiredDisplayModeSpecs(int defaultMode, boolean allowGroupSwitching,
-                float primaryRefreshRateMin, float primaryRefreshRateMax,
-                float appRequestRefreshRateMin, float appRequestRefreshRateMax) {
+                RefreshRateRanges primaryRanges, RefreshRateRanges appRequestRanges) {
             this.defaultMode = defaultMode;
             this.allowGroupSwitching = allowGroupSwitching;
-            this.primaryRefreshRateMin = primaryRefreshRateMin;
-            this.primaryRefreshRateMax = primaryRefreshRateMax;
-            this.appRequestRefreshRateMin = appRequestRefreshRateMin;
-            this.appRequestRefreshRateMax = appRequestRefreshRateMax;
+            this.primaryRanges =
+                    new RefreshRateRanges(primaryRanges.physical, primaryRanges.render);
+            this.appRequestRanges =
+                    new RefreshRateRanges(appRequestRanges.physical, appRequestRanges.render);
         }
 
         @Override
@@ -1732,10 +1878,9 @@
          */
         public boolean equals(DesiredDisplayModeSpecs other) {
             return other != null && defaultMode == other.defaultMode
-                    && primaryRefreshRateMin == other.primaryRefreshRateMin
-                    && primaryRefreshRateMax == other.primaryRefreshRateMax
-                    && appRequestRefreshRateMin == other.appRequestRefreshRateMin
-                    && appRequestRefreshRateMax == other.appRequestRefreshRateMax;
+                    && allowGroupSwitching == other.allowGroupSwitching
+                    && primaryRanges.equals(other.primaryRanges)
+                    && appRequestRanges.equals(other.appRequestRanges);
         }
 
         @Override
@@ -1748,18 +1893,17 @@
          */
         public void copyFrom(DesiredDisplayModeSpecs other) {
             defaultMode = other.defaultMode;
-            primaryRefreshRateMin = other.primaryRefreshRateMin;
-            primaryRefreshRateMax = other.primaryRefreshRateMax;
-            appRequestRefreshRateMin = other.appRequestRefreshRateMin;
-            appRequestRefreshRateMax = other.appRequestRefreshRateMax;
+            allowGroupSwitching = other.allowGroupSwitching;
+            primaryRanges.copyFrom(other.primaryRanges);
+            appRequestRanges.copyFrom(other.appRequestRanges);
         }
 
         @Override
         public String toString() {
-            return String.format("defaultConfig=%d primaryRefreshRateRange=[%.0f %.0f]"
-                            + " appRequestRefreshRateRange=[%.0f %.0f]",
-                    defaultMode, primaryRefreshRateMin, primaryRefreshRateMax,
-                    appRequestRefreshRateMin, appRequestRefreshRateMax);
+            return "defaultMode=" + defaultMode
+                    + " allowGroupSwitching=" + allowGroupSwitching
+                    + " primaryRanges=" + primaryRanges
+                    + " appRequestRanges=" + appRequestRanges;
         }
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index bfa1350..efda257 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -888,20 +888,18 @@
     static BLASTBufferQueue.TransactionHangCallback sTransactionHangCallback =
         new BLASTBufferQueue.TransactionHangCallback() {
             @Override
-            public void onTransactionHang(boolean isGPUHang) {
-                if (isGPUHang && !sAnrReported) {
-                    sAnrReported = true;
-                    try {
-                        ActivityManager.getService().appNotResponding(
-                            "Buffer processing hung up due to stuck fence. Indicates GPU hang");
-                    } catch (RemoteException e) {
-                        // We asked the system to crash us, but the system
-                        // already crashed. Unfortunately things may be
-                        // out of control.
-                    }
-                } else {
-                    // TODO: Do something with this later. For now we just ANR
-                    // in dequeue buffer later like we always have.
+            public void onTransactionHang(String reason) {
+                if (sAnrReported) {
+                    return;
+                }
+
+                sAnrReported = true;
+                try {
+                    ActivityManager.getService().appNotResponding(reason);
+                } catch (RemoteException e) {
+                    // We asked the system to crash us, but the system
+                    // already crashed. Unfortunately things may be
+                    // out of control.
                 }
             }
         };
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index eab3f2d..69eed0a 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -30,7 +30,9 @@
 import static android.view.inputmethod.InputMethodManagerProto.ACTIVE;
 import static android.view.inputmethod.InputMethodManagerProto.CUR_ID;
 import static android.view.inputmethod.InputMethodManagerProto.FULLSCREEN_MODE;
+import static android.view.inputmethod.InputMethodManagerProto.NEXT_SERVED_VIEW;
 import static android.view.inputmethod.InputMethodManagerProto.SERVED_CONNECTING;
+import static android.view.inputmethod.InputMethodManagerProto.SERVED_VIEW;
 
 import static com.android.internal.inputmethod.StartInputReason.BOUND_TO_IMMS;
 
@@ -763,39 +765,37 @@
                     forceFocus = true;
                 }
             }
-            startInputOnWindowFocusGain(viewForWindowFocus,
-                    windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
-        }
 
-        private void startInputOnWindowFocusGain(View focusedView,
-                @SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
-            int startInputFlags = getStartInputFlags(focusedView, 0);
+            final int softInputMode = windowAttribute.softInputMode;
+            final int windowFlags = windowAttribute.flags;
+
+            int startInputFlags = getStartInputFlags(viewForWindowFocus, 0);
             startInputFlags |= StartInputFlags.WINDOW_GAINED_FOCUS;
 
             ImeTracing.getInstance().triggerClientDump(
                     "InputMethodManager.DelegateImpl#startInputAsyncOnWindowFocusGain",
                     InputMethodManager.this, null /* icProto */);
 
-            final ViewRootImpl viewRootImpl;
+            boolean checkFocusResult;
             synchronized (mH) {
                 if (mCurRootView == null) {
                     return;
                 }
-                viewRootImpl = mCurRootView;
                 if (mRestartOnNextWindowFocus) {
                     if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus as true");
                     mRestartOnNextWindowFocus = false;
-                    forceNewFocus = true;
+                    forceFocus = true;
                 }
+                checkFocusResult = checkFocusInternalLocked(forceFocus, mCurRootView);
             }
 
-            if (checkFocusInternal(forceNewFocus, false, viewRootImpl)) {
+            if (checkFocusResult) {
                 // We need to restart input on the current focus view.  This
                 // should be done in conjunction with telling the system service
                 // about the window gaining focus, to help make the transition
                 // smooth.
                 if (startInputOnWindowFocusGainInternal(StartInputReason.WINDOW_FOCUS_GAIN,
-                        focusedView, startInputFlags, softInputMode, windowFlags)) {
+                        viewForWindowFocus, startInputFlags, softInputMode, windowFlags)) {
                     return;
                 }
             }
@@ -810,7 +810,7 @@
                 // ignore the result
                 mServiceInvoker.startInputOrWindowGainedFocus(
                         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
-                        focusedView.getWindowToken(), startInputFlags, softInputMode,
+                        viewForWindowFocus.getWindowToken(), startInputFlags, softInputMode,
                         windowFlags,
                         null,
                         null, null,
@@ -825,8 +825,15 @@
         }
 
         @Override
-        public void onScheduledCheckFocus(@NonNull ViewRootImpl viewRootImpl) {
-            checkFocusInternal(false, true, viewRootImpl);
+        public void onScheduledCheckFocus(ViewRootImpl viewRootImpl) {
+            synchronized (mH) {
+                if (!checkFocusInternalLocked(false, viewRootImpl)) {
+                    return;
+                }
+            }
+            startInputOnWindowFocusGainInternal(StartInputReason.SCHEDULED_CHECK_FOCUS,
+                    null /* focusedView */, 0 /* startInputFlags */, 0 /* softInputMode */,
+                    0 /* windowFlags */);
         }
 
         @Override
@@ -896,8 +903,6 @@
     /**
      * Checks whether the active input connection (if any) is for the given view.
      *
-     * TODO(b/182259171): Clean-up hasActiveConnection to simplify the logic.
-     *
      * Note that this method is only intended for restarting input after focus gain
      * (e.g. b/160391516), DO NOT leverage this method to do another check.
      */
@@ -908,7 +913,6 @@
             }
 
             return mServedInputConnection != null
-                    && mServedInputConnection.isActive()
                     && mServedInputConnection.isAssociatedWith(view);
         }
     }
@@ -1118,13 +1122,16 @@
                         if (mCurRootView == null) {
                             return;
                         }
-                        if (!checkFocusInternal(mRestartOnNextWindowFocus, false, mCurRootView)) {
+                        if (!checkFocusInternalLocked(mRestartOnNextWindowFocus, mCurRootView)) {
                             return;
                         }
-                        final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
-                                : StartInputReason.DEACTIVATED_BY_IMMS;
-                        startInputOnWindowFocusGainInternal(reason, null, 0, 0, 0);
+                        mCurrentEditorInfo = null;
+                        mCompletions = null;
+                        mServedConnecting = true;
                     }
+                    final int reason = active ? StartInputReason.ACTIVATED_BY_IMMS
+                            : StartInputReason.DEACTIVATED_BY_IMMS;
+                    startInputInner(reason, null, 0, 0, 0);
                     return;
                 }
                 case MSG_SET_INTERACTIVE: {
@@ -2338,8 +2345,7 @@
     }
 
     /**
-     * Called from {@link #checkFocusInternal(boolean, boolean, ViewRootImpl)},
-     * {@link #restartInput(View)}, {@link #MSG_BIND} or {@link #MSG_UNBIND}.
+     * Starts an input connection from the served view that gains the window focus.
      * Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
      * background thread may blocked by other methods which already inside {@code mH} lock.
      */
@@ -2653,52 +2659,47 @@
      */
     @UnsupportedAppUsage
     public void checkFocus() {
-        final ViewRootImpl viewRootImpl;
         synchronized (mH) {
             if (mCurRootView == null) {
                 return;
             }
-            viewRootImpl = mCurRootView;
+            if (!checkFocusInternalLocked(false /* forceNewFocus */, mCurRootView)) {
+                return;
+            }
         }
-        checkFocusInternal(false /* forceNewFocus */, true /* startInput */, viewRootImpl);
+        startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
+                null /* focusedView */,
+                0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
     }
 
     /**
      * Check the next served view if needs to start input.
      */
-    private boolean checkFocusInternal(boolean forceNewFocus, boolean startInput,
-            ViewRootImpl viewRootImpl) {
-        synchronized (mH) {
-            if (mCurRootView != viewRootImpl) {
-                return false;
-            }
-            if (mServedView == mNextServedView && !forceNewFocus) {
-                return false;
-            }
-            if (DEBUG) {
-                Log.v(TAG, "checkFocus: view=" + mServedView
-                        + " next=" + mNextServedView
-                        + " force=" + forceNewFocus
-                        + " package="
-                        + (mServedView != null ? mServedView.getContext().getPackageName()
-                        : "<none>"));
-            }
-            // Close the connection when no next served view coming.
-            if (mNextServedView == null) {
-                finishInputLocked();
-                closeCurrentInput();
-                return false;
-            }
-            mServedView = mNextServedView;
-            if (mServedInputConnection != null) {
-                mServedInputConnection.finishComposingTextFromImm();
-            }
+    @GuardedBy("mH")
+    private boolean checkFocusInternalLocked(boolean forceNewFocus, ViewRootImpl viewRootImpl) {
+        if (mCurRootView != viewRootImpl) {
+            return false;
         }
-
-        if (startInput) {
-            startInputOnWindowFocusGainInternal(StartInputReason.CHECK_FOCUS,
-                    null /* focusedView */,
-                    0 /* startInputFlags */, 0 /* softInputMode */, 0 /* windowFlags */);
+        if (mServedView == mNextServedView && !forceNewFocus) {
+            return false;
+        }
+        if (DEBUG) {
+            Log.v(TAG, "checkFocus: view=" + mServedView
+                    + " next=" + mNextServedView
+                    + " force=" + forceNewFocus
+                    + " package="
+                    + (mServedView != null ? mServedView.getContext().getPackageName()
+                    : "<none>"));
+        }
+        // Close the connection when no next served view coming.
+        if (mNextServedView == null) {
+            finishInputLocked();
+            closeCurrentInput();
+            return false;
+        }
+        mServedView = mNextServedView;
+        if (mServedInputConnection != null) {
+            mServedInputConnection.finishComposingTextFromImm();
         }
         return true;
     }
@@ -3991,6 +3992,8 @@
             proto.write(FULLSCREEN_MODE, mFullscreenMode);
             proto.write(ACTIVE, mActive);
             proto.write(SERVED_CONNECTING, mServedConnecting);
+            proto.write(SERVED_VIEW, Objects.toString(mServedView));
+            proto.write(NEXT_SERVED_VIEW, Objects.toString(mNextServedView));
             proto.end(token);
             if (mCurRootView != null) {
                 mCurRootView.dumpDebug(proto, VIEW_ROOT_IMPL);
diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java
index 121839b..bdfcb03 100644
--- a/core/java/android/view/inputmethod/InputMethodSubtype.java
+++ b/core/java/android/view/inputmethod/InputMethodSubtype.java
@@ -16,6 +16,7 @@
 
 package android.view.inputmethod;
 
+import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -23,6 +24,7 @@
 import android.content.res.Configuration;
 import android.icu.text.DisplayContext;
 import android.icu.text.LocaleDisplayNames;
+import android.icu.util.ULocale;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -74,6 +76,10 @@
     /** {@hide} */
     public static final int SUBTYPE_ID_NONE = 0;
 
+    private static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
+
+    private static final String UNDEFINED_LANGUAGE_TAG = "und";
+
     private final boolean mIsAuxiliary;
     private final boolean mOverridesImplicitlyEnabledSubtype;
     private final boolean mIsAsciiCapable;
@@ -90,6 +96,14 @@
     private volatile HashMap<String, String> mExtraValueHashMapCache;
 
     /**
+     * A volatile cache to optimize {@link #getCanonicalizedLanguageTag()}.
+     *
+     * <p>{@code null} means that the initial evaluation is not yet done.</p>
+     */
+    @Nullable
+    private volatile String mCachedCanonicalizedLanguageTag;
+
+    /**
      * InputMethodSubtypeBuilder is a builder class of InputMethodSubtype.
      * This class is designed to be used with
      * {@link android.view.inputmethod.InputMethodManager#setAdditionalInputMethodSubtypes}.
@@ -392,6 +406,65 @@
     }
 
     /**
+     * Returns a canonicalized BCP 47 Language Tag initialized with {@link #getLocaleObject()}.
+     *
+     * <p>This has an internal cache mechanism.  Subsequent calls are in general cheap and fast.</p>
+     *
+     * @return a canonicalized BCP 47 Language Tag initialized with {@link #getLocaleObject()}. An
+     *         empty string if {@link #getLocaleObject()} returns {@code null} or an empty
+     *         {@link Locale} object.
+     * @hide
+     */
+    @AnyThread
+    @NonNull
+    public String getCanonicalizedLanguageTag() {
+        final String cachedValue = mCachedCanonicalizedLanguageTag;
+        if (cachedValue != null) {
+            return cachedValue;
+        }
+
+        String result = null;
+        final Locale locale = getLocaleObject();
+        if (locale != null) {
+            final String langTag = locale.toLanguageTag();
+            if (!TextUtils.isEmpty(langTag)) {
+                result = ULocale.createCanonical(ULocale.forLanguageTag(langTag)).toLanguageTag();
+            }
+        }
+        result = TextUtils.emptyIfNull(result);
+        mCachedCanonicalizedLanguageTag = result;
+        return result;
+    }
+
+    /**
+     * Determines whether this {@link InputMethodSubtype} can be used as the key of mapping rules
+     * between {@link InputMethodSubtype} and hardware keyboard layout.
+     *
+     * <p>Note that in a future build may require different rules.  Design the system so that the
+     * system can automatically take care of any rule changes upon OTAs.</p>
+     *
+     * @return {@code true} if this {@link InputMethodSubtype} can be used as the key of mapping
+     *         rules between {@link InputMethodSubtype} and hardware keyboard layout.
+     * @hide
+     */
+    public boolean isSuitableForPhysicalKeyboardLayoutMapping() {
+        if (hashCode() == SUBTYPE_ID_NONE) {
+            return false;
+        }
+        if (!TextUtils.equals(getMode(), SUBTYPE_MODE_KEYBOARD)) {
+            return false;
+        }
+        if (isAuxiliary()) {
+            return false;
+        }
+        final String langTag = getCanonicalizedLanguageTag();
+        if (langTag.isEmpty() || TextUtils.equals(langTag, UNDEFINED_LANGUAGE_TAG)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * @return The mode of the subtype.
      */
     public String getMode() {
diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
index fa18eec..f2b7099 100644
--- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
@@ -214,7 +214,7 @@
         }
     }
 
-    public boolean isActive() {
+    private boolean isActive() {
         return mParentInputMethodManager.isActive() && !isFinished();
     }
 
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 57103e4..b5c58fb 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -151,7 +151,6 @@
 import android.util.FeatureFlagUtils;
 import android.util.IntArray;
 import android.util.Log;
-import android.util.Range;
 import android.util.SparseIntArray;
 import android.util.TypedValue;
 import android.view.AccessibilityIterators.TextSegmentIterator;
@@ -9317,40 +9316,42 @@
 
     /** @hide */
     public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) {
-        Range<Integer> range = getRangeForRect(
+        int[] range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getSelectionArea()),
                 gesture.getGranularity());
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        Selection.setSelection(getEditableText(), range.getLower(), range.getUpper());
+        Selection.setSelection(getEditableText(), range[0], range[1]);
         mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
     public int performHandwritingSelectRangeGesture(@NonNull SelectRangeGesture gesture) {
-        Range<Integer> startRange = getRangeForRect(
+        int[] startRange = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getSelectionStartArea()),
                 gesture.getGranularity());
         if (startRange == null) {
             return handleGestureFailure(gesture);
         }
-        Range<Integer> endRange = getRangeForRect(
+        int[] endRange = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getSelectionEndArea()),
                 gesture.getGranularity());
-        if (endRange == null || endRange.getUpper() <= startRange.getLower()) {
+        if (endRange == null) {
             return handleGestureFailure(gesture);
         }
-        Range<Integer> range = startRange.extend(endRange);
-        Selection.setSelection(getEditableText(), range.getLower(), range.getUpper());
+        int[] range = new int[] {
+                Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1])
+        };
+        Selection.setSelection(getEditableText(), range[0], range[1]);
         mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
     public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) {
-        Range<Integer> range = getRangeForRect(
+        int[] range = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getDeletionArea()),
                 gesture.getGranularity());
         if (range == null) {
@@ -9361,42 +9362,44 @@
             range = adjustHandwritingDeleteGestureRange(range);
         }
 
-        getEditableText().delete(range.getLower(), range.getUpper());
-        Selection.setSelection(getEditableText(), range.getLower());
+        getEditableText().delete(range[0], range[1]);
+        Selection.setSelection(getEditableText(), range[0]);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
     /** @hide */
     public int performHandwritingDeleteRangeGesture(@NonNull DeleteRangeGesture gesture) {
-        Range<Integer> startRange = getRangeForRect(
+        int[] startRange = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getDeletionStartArea()),
                 gesture.getGranularity());
         if (startRange == null) {
             return handleGestureFailure(gesture);
         }
-        Range<Integer> endRange = getRangeForRect(
+        int[] endRange = getRangeForRect(
                 convertFromScreenToContentCoordinates(gesture.getDeletionEndArea()),
                 gesture.getGranularity());
         if (endRange == null) {
             return handleGestureFailure(gesture);
         }
-        Range<Integer> range = startRange.extend(endRange);
+        int[] range = new int[] {
+                Math.min(startRange[0], endRange[0]), Math.max(startRange[1], endRange[1])
+        };
 
         if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
             range = adjustHandwritingDeleteGestureRange(range);
         }
 
-        getEditableText().delete(range.getLower(), range.getUpper());
-        Selection.setSelection(getEditableText(), range.getLower());
+        getEditableText().delete(range[0], range[1]);
+        Selection.setSelection(getEditableText(), range[0]);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
-    private Range<Integer> adjustHandwritingDeleteGestureRange(Range<Integer> range) {
+    private int[] adjustHandwritingDeleteGestureRange(int[] range) {
         // For handwriting delete gestures with word granularity, adjust the start and end offsets
         // to remove extra whitespace around the deleted text.
 
-        int start = range.getLower();
-        int end = range.getUpper();
+        int start = range[0];
+        int end = range[1];
 
         // If the deleted text is at the start of the text, the behavior is the same as the case
         // where the deleted text follows a new line character.
@@ -9425,7 +9428,7 @@
                 if (start == 0) break;
                 codePointBeforeStart = Character.codePointBefore(mText, start);
             } while (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart));
-            return new Range(start, end);
+            return new int[] {start, end};
         }
 
         if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
@@ -9444,7 +9447,7 @@
                 if (end == mText.length()) break;
                 codePointAtEnd = Character.codePointAt(mText, end);
             } while (TextUtils.isWhitespaceExceptNewline(codePointAtEnd));
-            return new Range(start, end);
+            return new int[] {start, end};
         }
 
         // Return the original range.
@@ -9494,14 +9497,14 @@
                 lineVerticalCenter + 0.1f,
                 Math.max(startPoint.x, endPoint.x),
                 lineVerticalCenter - 0.1f);
-        Range<Integer> range = mLayout.getRangeForRect(
+        int[] range = mLayout.getRangeForRect(
                 area, new GraphemeClusterSegmentFinder(mText, mTextPaint),
                 Layout.INCLUSION_STRATEGY_ANY_OVERLAP);
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        int startOffset = range.getLower();
-        int endOffset = range.getUpper();
+        int startOffset = range[0];
+        int endOffset = range[1];
         // TODO(b/247557062): This doesn't handle bidirectional text correctly.
 
         Pattern whitespacePattern = getWhitespacePattern();
@@ -9606,7 +9609,7 @@
     }
 
     @Nullable
-    private Range<Integer> getRangeForRect(@NonNull RectF area, int granularity) {
+    private int[] getRangeForRect(@NonNull RectF area, int granularity) {
         SegmentFinder segmentFinder;
         if (granularity == HandwritingGesture.GRANULARITY_WORD) {
             WordIterator wordIterator = getWordIterator();
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 4a4f561..85b2881 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -18,8 +18,10 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.view.RemoteAnimationTarget;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -50,6 +52,8 @@
 
     @SwipeEdge
     private final int mSwipeEdge;
+    @Nullable
+    private final RemoteAnimationTarget mDepartingAnimationTarget;
 
     /**
      * Creates a new {@link BackEvent} instance.
@@ -58,12 +62,16 @@
      * @param touchY Absolute Y location of the touch point of this event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param swipeEdge Indicates which edge the swipe starts from.
+     * @param departingAnimationTarget The remote animation target of the departing
+     *                                 application window.
      */
-    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge) {
+    public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
+            @Nullable RemoteAnimationTarget departingAnimationTarget) {
         mTouchX = touchX;
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
+        mDepartingAnimationTarget = departingAnimationTarget;
     }
 
     private BackEvent(@NonNull Parcel in) {
@@ -71,6 +79,7 @@
         mTouchY = in.readFloat();
         mProgress = in.readFloat();
         mSwipeEdge = in.readInt();
+        mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
     }
 
     public static final Creator<BackEvent> CREATOR = new Creator<BackEvent>() {
@@ -96,6 +105,7 @@
         dest.writeFloat(mTouchY);
         dest.writeFloat(mProgress);
         dest.writeInt(mSwipeEdge);
+        dest.writeTypedObject(mDepartingAnimationTarget, flags);
     }
 
     /**
@@ -126,6 +136,16 @@
         return mSwipeEdge;
     }
 
+    /**
+     * Returns the {@link RemoteAnimationTarget} of the top departing application window,
+     * or {@code null} if the top window should not be moved for the current type of back
+     * destination.
+     */
+    @Nullable
+    public RemoteAnimationTarget getDepartingAnimationTarget() {
+        return mDepartingAnimationTarget;
+    }
+
     @Override
     public String toString() {
         return "BackEvent{"
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
new file mode 100644
index 0000000..2e3afde
--- /dev/null
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.util.FloatProperty;
+
+import com.android.internal.dynamicanimation.animation.SpringAnimation;
+import com.android.internal.dynamicanimation.animation.SpringForce;
+
+/**
+ * An animator that drives the predictive back progress with a spring.
+ *
+ * The back gesture's latest touch point and committal state determines the final position of
+ * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with
+ * smoothly transitioning progress values.
+ *
+ * @hide
+ */
+public class BackProgressAnimator {
+    /**
+     *  A factor to scale the input progress by, so that it works better with the spring.
+     *  We divide the output progress by this value before sending it to apps, so that apps
+     *  always receive progress values in [0, 1].
+     */
+    private static final float SCALE_FACTOR = 100f;
+    private final SpringAnimation mSpring;
+    private ProgressCallback mCallback;
+    private float mProgress = 0;
+    private BackEvent mLastBackEvent;
+    private boolean mStarted = false;
+
+    private void setProgress(float progress) {
+        mProgress = progress;
+    }
+
+    private float getProgress() {
+        return mProgress;
+    }
+
+    private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP =
+            new FloatProperty<BackProgressAnimator>("progress") {
+                @Override
+                public void setValue(BackProgressAnimator animator, float value) {
+                    animator.setProgress(value);
+                    animator.updateProgressValue(value);
+                }
+
+                @Override
+                public Float get(BackProgressAnimator object) {
+                    return object.getProgress();
+                }
+            };
+
+
+    /** A callback to be invoked when there's a progress value update from the animator. */
+    public interface ProgressCallback {
+        /** Called when there's a progress value update. */
+        void onProgressUpdate(BackEvent event);
+    }
+
+    public BackProgressAnimator() {
+        mSpring = new SpringAnimation(this, PROGRESS_PROP);
+        mSpring.setSpring(new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
+    }
+
+    /**
+     * Sets a new target position for the back progress.
+     *
+     * @param event the {@link BackEvent} containing the latest target progress.
+     */
+    public void onBackProgressed(BackEvent event) {
+        if (!mStarted) {
+            return;
+        }
+        mLastBackEvent = event;
+        if (mSpring == null) {
+            return;
+        }
+        mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR);
+    }
+
+    /**
+     * Starts the back progress animation.
+     *
+     * @param event the {@link BackEvent} that started the gesture.
+     * @param callback the back callback to invoke for the gesture. It will receive back progress
+     *                 dispatches as the progress animation updates.
+     */
+    public void onBackStarted(BackEvent event, ProgressCallback callback) {
+        reset();
+        mLastBackEvent = event;
+        mCallback = callback;
+        mStarted = true;
+    }
+
+    /**
+     * Resets the back progress animation. This should be called when back is invoked or cancelled.
+     */
+    public void reset() {
+        mSpring.animateToFinalPosition(0);
+        if (mSpring.canSkipToEnd()) {
+            mSpring.skipToEnd();
+        } else {
+            // Should never happen.
+            mSpring.cancel();
+        }
+        mStarted = false;
+        mLastBackEvent = null;
+        mCallback = null;
+        mProgress = 0;
+    }
+
+    private void updateProgressValue(float progress) {
+        if (mLastBackEvent == null || mCallback == null || !mStarted) {
+            return;
+        }
+        mCallback.onProgressUpdate(
+                new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
+                        progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge(),
+                        mLastBackEvent.getDepartingAnimationTarget()));
+    }
+
+}
diff --git a/core/java/android/window/IOnBackInvokedCallback.aidl b/core/java/android/window/IOnBackInvokedCallback.aidl
index 47796de..6af8ddd 100644
--- a/core/java/android/window/IOnBackInvokedCallback.aidl
+++ b/core/java/android/window/IOnBackInvokedCallback.aidl
@@ -28,17 +28,18 @@
 oneway interface IOnBackInvokedCallback {
    /**
     * Called when a back gesture has been started, or back button has been pressed down.
-    * Wraps {@link OnBackInvokedCallback#onBackStarted()}.
+    * Wraps {@link OnBackInvokedCallback#onBackStarted(BackEvent)}.
+    *
+    * @param backEvent The {@link BackEvent} containing information about the touch or button press.
     */
-    void onBackStarted();
+    void onBackStarted(in BackEvent backEvent);
 
     /**
      * Called on back gesture progress.
-     * Wraps {@link OnBackInvokedCallback#onBackProgressed()}.
+     * Wraps {@link OnBackInvokedCallback#onBackProgressed(BackEvent)}.
      *
-     * @param touchX Absolute X location of the touch point.
-     * @param touchY Absolute Y location of the touch point.
-     * @param progress Value between 0 and 1 on how far along the back gesture is.
+     * @param backEvent The {@link BackEvent} containing information about the latest touch point
+     *                  and the progress that the back animation should seek to.
      */
     void onBackProgressed(in BackEvent backEvent);
 
diff --git a/core/java/android/window/OnBackAnimationCallback.java b/core/java/android/window/OnBackAnimationCallback.java
index 1a37e57..c05809b 100644
--- a/core/java/android/window/OnBackAnimationCallback.java
+++ b/core/java/android/window/OnBackAnimationCallback.java
@@ -13,14 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package android.window;
-
 import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.View;
-
 /**
  * Interface for applications to register back animation callbacks along their custom back
  * handling.
@@ -40,11 +37,10 @@
  * @hide
  */
 public interface OnBackAnimationCallback extends OnBackInvokedCallback {
-   /**
-    * Called when a back gesture has been started, or back button has been pressed down.
-    */
+    /**
+     * Called when a back gesture has been started, or back button has been pressed down.
+     */
     default void onBackStarted() { }
-
     /**
      * Called on back gesture progress.
      *
@@ -53,7 +49,6 @@
      * @see BackEvent
      */
     default void onBackProgressed(@NonNull BackEvent backEvent) { }
-
     /**
      * Called when a back gesture or back button press has been cancelled.
      */
diff --git a/core/java/android/window/OnBackInvokedCallback.java b/core/java/android/window/OnBackInvokedCallback.java
index 6e2d4f9..62c41bf 100644
--- a/core/java/android/window/OnBackInvokedCallback.java
+++ b/core/java/android/window/OnBackInvokedCallback.java
@@ -16,6 +16,7 @@
 
 package android.window;
 
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.Dialog;
 import android.view.Window;
@@ -41,8 +42,35 @@
 @SuppressWarnings("deprecation")
 public interface OnBackInvokedCallback {
     /**
+     * Called when a back gesture has been started, or back button has been pressed down.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the touch or
+     *                  button press.
+     *
+     * @hide
+     */
+    default void onBackStarted(@NonNull BackEvent backEvent) {}
+
+    /**
+     * Called when a back gesture has been progressed.
+     *
+     * @param backEvent The {@link BackEvent} containing information about the latest touch point
+     *                  and the progress that the back animation should seek to.
+     *
+     * @hide
+     */
+    default void onBackProgressed(@NonNull BackEvent backEvent) {}
+
+    /**
      * Called when a back gesture has been completed and committed, or back button pressed
      * has been released and committed.
      */
     void onBackInvoked();
+
+    /**
+     * Called when a back gesture or button press has been cancelled.
+     *
+     * @hide
+     */
+    default void onBackCancelled() {}
 }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 0730f3d..fda39c1 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -218,19 +218,24 @@
     public Checker getChecker() {
         return mChecker;
     }
+    @NonNull
+    private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
 
     static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
         private final WeakReference<OnBackInvokedCallback> mCallback;
+
         OnBackInvokedCallbackWrapper(@NonNull OnBackInvokedCallback callback) {
             mCallback = new WeakReference<>(callback);
         }
 
         @Override
-        public void onBackStarted() {
+        public void onBackStarted(BackEvent backEvent) {
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
-                    callback.onBackStarted();
+                    mProgressAnimator.onBackStarted(backEvent, event ->
+                            callback.onBackProgressed(event));
+                    callback.onBackStarted(backEvent);
                 }
             });
         }
@@ -240,7 +245,7 @@
             Handler.getMain().post(() -> {
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
-                    callback.onBackProgressed(backEvent);
+                    mProgressAnimator.onBackProgressed(backEvent);
                 }
             });
         }
@@ -248,6 +253,7 @@
         @Override
         public void onBackCancelled() {
             Handler.getMain().post(() -> {
+                mProgressAnimator.reset();
                 final OnBackAnimationCallback callback = getBackAnimationCallback();
                 if (callback != null) {
                     callback.onBackCancelled();
@@ -258,6 +264,7 @@
         @Override
         public void onBackInvoked() throws RemoteException {
             Handler.getMain().post(() -> {
+                mProgressAnimator.reset();
                 final OnBackInvokedCallback callback = mCallback.get();
                 if (callback == null) {
                     return;
diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java
index 999be08..65372be 100644
--- a/core/java/com/android/internal/app/LocalePicker.java
+++ b/core/java/com/android/internal/app/LocalePicker.java
@@ -314,8 +314,7 @@
 
         try {
             final IActivityManager am = ActivityManager.getService();
-            final Configuration config = am.getConfiguration();
-
+            final Configuration config = new Configuration();
             config.setLocales(locales);
             config.userSetLocale = true;
 
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 4a1f7eb..42b46cd 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -647,15 +647,16 @@
 
         if (info instanceof DisplayResolveInfo) {
             DisplayResolveInfo dri = (DisplayResolveInfo) info;
-            boolean hasLabel = dri.hasDisplayLabel();
-            holder.bindLabel(
-                    dri.getDisplayLabel(),
-                    dri.getExtendedInfo(),
-                    hasLabel && alwaysShowSubLabel());
-            holder.bindIcon(info);
-            if (!hasLabel) {
+            if (dri.hasDisplayLabel()) {
+                holder.bindLabel(
+                        dri.getDisplayLabel(),
+                        dri.getExtendedInfo(),
+                        alwaysShowSubLabel());
+            } else {
+                holder.bindLabel("", "", false);
                 loadLabel(dri);
             }
+            holder.bindIcon(info);
             if (!dri.hasDisplayIcon()) {
                 loadIcon(dri);
             }
diff --git a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
index e748982..8e7207f 100644
--- a/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
+++ b/core/java/com/android/internal/appwidget/IAppWidgetService.aidl
@@ -45,6 +45,7 @@
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId);
     int[] getAppWidgetIdsForHost(String callingPackage, int hostId);
+    void setAppWidgetHidden(in String callingPackage, int hostId);
     IntentSender createAppWidgetConfigIntentSender(String callingPackage, int appWidgetId,
             int intentFlags);
 
diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl
index f09e176..21c7baa 100644
--- a/core/java/com/android/internal/backup/IBackupTransport.aidl
+++ b/core/java/com/android/internal/backup/IBackupTransport.aidl
@@ -16,6 +16,7 @@
 
 package com.android.internal.backup;
 
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
@@ -400,4 +401,13 @@
      * <p>For supported flags see {@link android.app.backup.BackupAgent}.
      */
     void getTransportFlags(in AndroidFuture<int> resultFuture);
+
+    /**
+     * Ask the transport for a {@link IBackupManagerMonitor} instance which will be used by the
+     * framework to report logging events back to the transport.
+     *
+     * Backups requested from outside the framework may pass in a monitor with the request,
+     * however backups initiated by the framework will call this method to retrieve one.
+     */
+    void getBackupManagerMonitor(in AndroidFuture<IBackupManagerMonitor> resultFuture);
 }
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java b/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java
index f0fe573..5392bdc 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java
+++ b/core/java/com/android/internal/inputmethod/IInputMethodManagerGlobal.java
@@ -155,4 +155,21 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Invokes {@link IInputMethodManager#removeImeSurface()}
+     */
+    @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)
+    @AnyThread
+    public static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
+        final IInputMethodManager service = getService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.removeImeSurface();
+        } catch (RemoteException e) {
+            handleRemoteExceptionOrRethrow(e, exceptionHandler);
+        }
+    }
 }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index 09c97b3..1b4afd6 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -49,6 +49,8 @@
                 return "WINDOW_FOCUS_GAIN";
             case StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY:
                 return "WINDOW_FOCUS_GAIN_REPORT_ONLY";
+            case StartInputReason.SCHEDULED_CHECK_FOCUS:
+                return "SCHEDULED_CHECK_FOCUS";
             case StartInputReason.APP_CALLED_RESTART_INPUT_API:
                 return "APP_CALLED_RESTART_INPUT_API";
             case StartInputReason.CHECK_FOCUS:
diff --git a/core/java/com/android/internal/inputmethod/StartInputReason.java b/core/java/com/android/internal/inputmethod/StartInputReason.java
index 51ed841..733d975 100644
--- a/core/java/com/android/internal/inputmethod/StartInputReason.java
+++ b/core/java/com/android/internal/inputmethod/StartInputReason.java
@@ -31,6 +31,7 @@
         StartInputReason.UNSPECIFIED,
         StartInputReason.WINDOW_FOCUS_GAIN,
         StartInputReason.WINDOW_FOCUS_GAIN_REPORT_ONLY,
+        StartInputReason.SCHEDULED_CHECK_FOCUS,
         StartInputReason.APP_CALLED_RESTART_INPUT_API,
         StartInputReason.CHECK_FOCUS,
         StartInputReason.BOUND_TO_IMMS,
@@ -58,6 +59,11 @@
      */
     int WINDOW_FOCUS_GAIN_REPORT_ONLY = 2;
     /**
+     * Similar to {@link #CHECK_FOCUS}, but the one scheduled with
+     * {@link android.view.ViewRootImpl#dispatchCheckFocus()}.
+     */
+    int SCHEDULED_CHECK_FOCUS = 3;
+    /**
      * {@link android.view.inputmethod.InputMethodManager#restartInput(android.view.View)} is
      * either explicitly called by the application or indirectly called by some Framework class
      * (e.g. {@link android.widget.EditText}).
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 76f33a6..b0d5922 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -45,6 +45,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_ENTER_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__ONE_HANDED_EXIT_TRANSITION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PIP_TRANSITION;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SCREEN_OFF_SHOW_AOD;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_PAGE_SCROLL;
@@ -224,6 +225,7 @@
     public static final int CUJ_SHADE_CLEAR_ALL = 62;
     public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63;
     public static final int CUJ_LOCKSCREEN_OCCLUSION = 64;
+    public static final int CUJ_RECENTS_SCROLLING = 65;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -297,6 +299,7 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING,
     };
 
     private static class InstanceHolder {
@@ -385,7 +388,8 @@
             CUJ_TASKBAR_COLLAPSE,
             CUJ_SHADE_CLEAR_ALL,
             CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
-            CUJ_LOCKSCREEN_OCCLUSION
+            CUJ_LOCKSCREEN_OCCLUSION,
+            CUJ_RECENTS_SCROLLING
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -900,6 +904,8 @@
                 return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION";
             case CUJ_LOCKSCREEN_OCCLUSION:
                 return "LOCKSCREEN_OCCLUSION";
+            case CUJ_RECENTS_SCROLLING:
+                return "RECENTS_SCROLLING";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 681b46a..0489dc81 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -35,7 +35,10 @@
 
 // Manages the NotificationChannels used by the frameworks itself.
 public class SystemNotificationChannels {
-    public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
+    /**
+     * @deprecated Legacy system channel, which is no longer used,
+     */
+    @Deprecated public static String VIRTUAL_KEYBOARD  = "VIRTUAL_KEYBOARD";
     public static String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD";
     public static String SECURITY = "SECURITY";
     public static String CAR_MODE = "CAR_MODE";
@@ -72,13 +75,6 @@
     public static void createAll(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
         List<NotificationChannel> channelsList = new ArrayList<NotificationChannel>();
-        final NotificationChannel keyboard = new NotificationChannel(
-                VIRTUAL_KEYBOARD,
-                context.getString(R.string.notification_channel_virtual_keyboard),
-                NotificationManager.IMPORTANCE_LOW);
-        keyboard.setBlockable(true);
-        channelsList.add(keyboard);
-
         final NotificationChannel physicalKeyboardChannel = new NotificationChannel(
                 PHYSICAL_KEYBOARD,
                 context.getString(R.string.notification_channel_physical_keyboard),
@@ -237,6 +233,7 @@
     /** Remove notification channels which are no longer used */
     public static void removeDeprecated(Context context) {
         final NotificationManager nm = context.getSystemService(NotificationManager.class);
+        nm.deleteNotificationChannel(VIRTUAL_KEYBOARD);
         nm.deleteNotificationChannel(DEVICE_ADMIN_DEPRECATED);
         nm.deleteNotificationChannel(SYSTEM_CHANGES_DEPRECATED);
     }
diff --git a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
index a09c823..04dd2d7 100644
--- a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
+++ b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java
@@ -97,7 +97,6 @@
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
@@ -224,7 +223,6 @@
             }
 
             case KeyEvent.KEYCODE_HEADSETHOOK:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 295dc54..25ac1bd 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -41,12 +41,16 @@
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorSpace;
 import android.graphics.Picture;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.hardware.HardwareBuffer;
+import android.media.Image;
+import android.media.ImageReader;
 import android.os.SystemProperties;
 import android.util.Slog;
+import android.view.SurfaceControl;
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManager.TransitionOldType;
 import android.view.WindowManager.TransitionType;
@@ -59,9 +63,11 @@
 import android.view.animation.PathInterpolator;
 import android.view.animation.ScaleAnimation;
 import android.view.animation.TranslateAnimation;
+import android.window.ScreenCapture;
 
 import com.android.internal.R;
 
+import java.nio.ByteBuffer;
 import java.util.List;
 
 /** @hide */
@@ -1262,4 +1268,90 @@
 
         return set;
     }
+
+    /** Returns whether the hardware buffer passed in is marked as protected. */
+    public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
+        return (hardwareBuffer.getUsage() & HardwareBuffer.USAGE_PROTECTED_CONTENT)
+                == HardwareBuffer.USAGE_PROTECTED_CONTENT;
+    }
+
+    /** Returns the luminance in 0~1. */
+    public static float getBorderLuma(SurfaceControl surfaceControl, int w, int h) {
+        final ScreenCapture.ScreenshotHardwareBuffer buffer =
+                ScreenCapture.captureLayers(surfaceControl, new Rect(0, 0, w, h), 1);
+        if (buffer != null) {
+            return getBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
+        }
+        return 0;
+    }
+
+    /** Returns the luminance in 0~1. */
+    public static float getBorderLuma(HardwareBuffer hwBuffer, ColorSpace colorSpace) {
+        if (hwBuffer == null) {
+            return 0;
+        }
+        final int format = hwBuffer.getFormat();
+        // Only support RGB format in 4 bytes. And protected buffer is not readable.
+        if (format != HardwareBuffer.RGBA_8888 || hasProtectedContent(hwBuffer)) {
+            return 0;
+        }
+
+        final ImageReader ir = ImageReader.newInstance(hwBuffer.getWidth(), hwBuffer.getHeight(),
+                format, 1 /* maxImages */);
+        ir.getSurface().attachAndQueueBufferWithColorSpace(hwBuffer, colorSpace);
+        final Image image = ir.acquireLatestImage();
+        if (image == null || image.getPlaneCount() < 1) {
+            return 0;
+        }
+
+        final Image.Plane plane = image.getPlanes()[0];
+        final ByteBuffer buffer = plane.getBuffer();
+        final int width = image.getWidth();
+        final int height = image.getHeight();
+        final int pixelStride = plane.getPixelStride();
+        final int rowStride = plane.getRowStride();
+        final int sampling = 10;
+        final int[] borderLumas = new int[(width + height) * 2 / sampling];
+
+        // Grab the top and bottom borders.
+        int i = 0;
+        for (int x = 0, size = width - sampling; x < size; x += sampling) {
+            borderLumas[i++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
+            borderLumas[i++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
+        }
+
+        // Grab the left and right borders.
+        for (int y = 0, size = height - sampling; y < size; y += sampling) {
+            borderLumas[i++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
+            borderLumas[i++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
+        }
+
+        ir.close();
+
+        // Get "mode" by histogram.
+        final int[] histogram = new int[256];
+        int maxCount = 0;
+        int mostLuma = 0;
+        for (int luma : borderLumas) {
+            final int count = ++histogram[luma];
+            if (count > maxCount) {
+                maxCount = count;
+                mostLuma = luma;
+            }
+        }
+        return mostLuma / 255f;
+    }
+
+    /** Returns the luminance of the pixel in 0~255. */
+    private static int getPixelLuminance(ByteBuffer buffer, int x, int y, int pixelStride,
+            int rowStride) {
+        final int color = buffer.getInt(y * rowStride + x * pixelStride);
+        // The buffer from ImageReader is always in native order (little-endian), so extract the
+        // color components in reversed order.
+        final int r = color & 0xff;
+        final int g = (color >> 8) & 0xff;
+        final int b = (color >> 16) & 0xff;
+        // Approximation of WCAG 2.0 relative luminance.
+        return ((r * 8) + (g * 22) + (b * 2)) >> 5;
+    }
 }
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 44cfe1a..1d4b246 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -322,4 +322,7 @@
 
     /** Unregisters a nearby media devices provider. */
     void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider);
+
+    /** Dump protos from SystemUI. The proto definition is defined there */
+    void dumpProto(in String[] args, in ParcelFileDescriptor pfd);
 }
diff --git a/core/java/com/android/internal/util/ArtBinaryXmlPullParser.java b/core/java/com/android/internal/util/ArtBinaryXmlPullParser.java
new file mode 100644
index 0000000..c56bc49
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtBinaryXmlPullParser.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import android.annotation.NonNull;
+
+import com.android.modules.utils.BinaryXmlPullParser;
+import com.android.modules.utils.FastDataInput;
+
+import java.io.DataInput;
+import java.io.InputStream;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This decodes large code-points using 4-byte sequences, and <em>is not</em> compatible with the
+ * {@link DataInput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtBinaryXmlPullParser extends BinaryXmlPullParser {
+    @NonNull
+    protected FastDataInput obtainFastDataInput(@NonNull InputStream is) {
+        return ArtFastDataInput.obtain(is);
+    }
+}
diff --git a/core/java/com/android/internal/util/ArtBinaryXmlSerializer.java b/core/java/com/android/internal/util/ArtBinaryXmlSerializer.java
new file mode 100644
index 0000000..98a2135
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtBinaryXmlSerializer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import android.annotation.NonNull;
+
+import com.android.modules.utils.BinaryXmlSerializer;
+import com.android.modules.utils.FastDataOutput;
+
+import java.io.DataOutput;
+import java.io.OutputStream;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This encodes large code-points using 4-byte sequences and <em>is not</em> compatible with the
+ * {@link DataOutput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtBinaryXmlSerializer extends BinaryXmlSerializer {
+    @NonNull
+    @Override
+    protected FastDataOutput obtainFastDataOutput(@NonNull OutputStream os) {
+        return ArtFastDataOutput.obtain(os);
+    }
+}
diff --git a/core/java/com/android/internal/util/ArtFastDataInput.java b/core/java/com/android/internal/util/ArtFastDataInput.java
new file mode 100644
index 0000000..3e8916c
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtFastDataInput.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import android.annotation.NonNull;
+import android.util.CharsetUtils;
+
+import com.android.modules.utils.FastDataInput;
+
+import java.io.DataInput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This decodes large code-points using 4-byte sequences, and <em>is not</em> compatible with the
+ * {@link DataInput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtFastDataInput extends FastDataInput {
+    private static AtomicReference<ArtFastDataInput> sInCache = new AtomicReference<>();
+
+    private final long mBufferPtr;
+
+    public ArtFastDataInput(@NonNull InputStream in, int bufferSize) {
+        super(in, bufferSize);
+
+        mBufferPtr = mRuntime.addressOf(mBuffer);
+    }
+
+    /**
+     * Obtain a {@link ArtFastDataInput} configured with the given
+     * {@link InputStream} and which decodes large code-points using 4-byte
+     * sequences.
+     * <p>
+     * This <em>is not</em> compatible with the {@link DataInput} API contract,
+     * which specifies that large code-points must be encoded with 3-byte
+     * sequences.
+     */
+    public static ArtFastDataInput obtain(@NonNull InputStream in) {
+        ArtFastDataInput instance = sInCache.getAndSet(null);
+        if (instance != null) {
+            instance.setInput(in);
+            return instance;
+        }
+        return new ArtFastDataInput(in, DEFAULT_BUFFER_SIZE);
+    }
+
+    /**
+     * Release a {@link ArtFastDataInput} to potentially be recycled. You must not
+     * interact with the object after releasing it.
+     */
+    public void release() {
+        super.release();
+
+        if (mBufferCap == DEFAULT_BUFFER_SIZE) {
+            // Try to return to the cache.
+            sInCache.compareAndSet(null, this);
+        }
+    }
+
+    @Override
+    public String readUTF() throws IOException {
+        // Attempt to read directly from buffer space if there's enough room,
+        // otherwise fall back to chunking into place
+        final int len = readUnsignedShort();
+        if (mBufferCap > len) {
+            if (mBufferLim - mBufferPos < len) fill(len);
+            final String res = CharsetUtils.fromModifiedUtf8Bytes(mBufferPtr, mBufferPos, len);
+            mBufferPos += len;
+            return res;
+        } else {
+            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
+            readFully(tmp, 0, len);
+            return CharsetUtils.fromModifiedUtf8Bytes(mRuntime.addressOf(tmp), 0, len);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/util/ArtFastDataOutput.java b/core/java/com/android/internal/util/ArtFastDataOutput.java
new file mode 100644
index 0000000..ac595b6
--- /dev/null
+++ b/core/java/com/android/internal/util/ArtFastDataOutput.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import android.annotation.NonNull;
+import android.util.CharsetUtils;
+
+import com.android.modules.utils.FastDataOutput;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * {@inheritDoc}
+ * <p>
+ * This encodes large code-points using 4-byte sequences and <em>is not</em> compatible with the
+ * {@link DataOutput} API contract, which specifies that large code-points must be encoded with
+ * 3-byte sequences.
+ */
+public class ArtFastDataOutput extends FastDataOutput {
+    private static AtomicReference<ArtFastDataOutput> sOutCache = new AtomicReference<>();
+
+    private final long mBufferPtr;
+
+    public ArtFastDataOutput(@NonNull OutputStream out, int bufferSize) {
+        super(out, bufferSize);
+
+        mBufferPtr = mRuntime.addressOf(mBuffer);
+    }
+
+    /**
+     * Obtain an {@link ArtFastDataOutput} configured with the given
+     * {@link OutputStream} and which encodes large code-points using 4-byte
+     * sequences.
+     * <p>
+     * This <em>is not</em> compatible with the {@link DataOutput} API contract,
+     * which specifies that large code-points must be encoded with 3-byte
+     * sequences.
+     */
+    public static ArtFastDataOutput obtain(@NonNull OutputStream out) {
+        ArtFastDataOutput instance = sOutCache.getAndSet(null);
+        if (instance != null) {
+            instance.setOutput(out);
+            return instance;
+        }
+        return new ArtFastDataOutput(out, DEFAULT_BUFFER_SIZE);
+    }
+
+    @Override
+    public void release() {
+        super.release();
+
+        if (mBufferCap == DEFAULT_BUFFER_SIZE) {
+            // Try to return to the cache.
+            sOutCache.compareAndSet(null, this);
+        }
+    }
+
+    @Override
+    public void writeUTF(String s) throws IOException {
+        // Attempt to write directly to buffer space if there's enough room,
+        // otherwise fall back to chunking into place
+        if (mBufferCap - mBufferPos < 2 + s.length()) drain();
+
+        // Magnitude of this returned value indicates the number of bytes
+        // required to encode the string; sign indicates success/failure
+        int len = CharsetUtils.toModifiedUtf8Bytes(s, mBufferPtr, mBufferPos + 2, mBufferCap);
+        if (Math.abs(len) > MAX_UNSIGNED_SHORT) {
+            throw new IOException("Modified UTF-8 length too large: " + len);
+        }
+
+        if (len >= 0) {
+            // Positive value indicates the string was encoded into the buffer
+            // successfully, so we only need to prefix with length
+            writeShort(len);
+            mBufferPos += len;
+        } else {
+            // Negative value indicates buffer was too small and we need to
+            // allocate a temporary buffer for encoding
+            len = -len;
+            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
+            CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
+            writeShort(len);
+            write(tmp, 0, len);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/util/BinaryXmlPullParser.java b/core/java/com/android/internal/util/BinaryXmlPullParser.java
deleted file mode 100644
index d3abac9..0000000
--- a/core/java/com/android/internal/util/BinaryXmlPullParser.java
+++ /dev/null
@@ -1,939 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import static com.android.internal.util.BinaryXmlSerializer.ATTRIBUTE;
-import static com.android.internal.util.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_BASE64;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_HEX;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_DOUBLE;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_FLOAT;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT_HEX;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG_HEX;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_NULL;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING;
-import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING_INTERNED;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Objects;
-
-/**
- * Parser that reads XML documents using a custom binary wire protocol which
- * benchmarking has shown to be 8.5x faster than {@link Xml.newFastPullParser()}
- * for a typical {@code packages.xml}.
- * <p>
- * The high-level design of the wire protocol is to directly serialize the event
- * stream, while efficiently and compactly writing strongly-typed primitives
- * delivered through the {@link TypedXmlSerializer} interface.
- * <p>
- * Each serialized event is a single byte where the lower half is a normal
- * {@link XmlPullParser} token and the upper half is an optional data type
- * signal, such as {@link #TYPE_INT}.
- * <p>
- * This parser has some specific limitations:
- * <ul>
- * <li>Only the UTF-8 encoding is supported.
- * <li>Variable length values, such as {@code byte[]} or {@link String}, are
- * limited to 65,535 bytes in length. Note that {@link String} values are stored
- * as UTF-8 on the wire.
- * <li>Namespaces, prefixes, properties, and options are unsupported.
- * </ul>
- */
-public final class BinaryXmlPullParser implements TypedXmlPullParser {
-    private FastDataInput mIn;
-
-    private int mCurrentToken = START_DOCUMENT;
-    private int mCurrentDepth = 0;
-    private String mCurrentName;
-    private String mCurrentText;
-
-    /**
-     * Pool of attributes parsed for the currently tag. All interactions should
-     * be done via {@link #obtainAttribute()}, {@link #findAttribute(String)},
-     * and {@link #resetAttributes()}.
-     */
-    private int mAttributeCount = 0;
-    private Attribute[] mAttributes;
-
-    @Override
-    public void setInput(InputStream is, String encoding) throws XmlPullParserException {
-        if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
-            throw new UnsupportedOperationException();
-        }
-
-        if (mIn != null) {
-            mIn.release();
-            mIn = null;
-        }
-
-        mIn = FastDataInput.obtainUsing4ByteSequences(is);
-
-        mCurrentToken = START_DOCUMENT;
-        mCurrentDepth = 0;
-        mCurrentName = null;
-        mCurrentText = null;
-
-        mAttributeCount = 0;
-        mAttributes = new Attribute[8];
-        for (int i = 0; i < mAttributes.length; i++) {
-            mAttributes[i] = new Attribute();
-        }
-
-        try {
-            final byte[] magic = new byte[4];
-            mIn.readFully(magic);
-            if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) {
-                throw new IOException("Unexpected magic " + bytesToHexString(magic));
-            }
-
-            // We're willing to immediately consume a START_DOCUMENT if present,
-            // but we're okay if it's missing
-            if (peekNextExternalToken() == START_DOCUMENT) {
-                consumeToken();
-            }
-        } catch (IOException e) {
-            throw new XmlPullParserException(e.toString());
-        }
-    }
-
-    @Override
-    public void setInput(Reader in) throws XmlPullParserException {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public int next() throws XmlPullParserException, IOException {
-        while (true) {
-            final int token = nextToken();
-            switch (token) {
-                case START_TAG:
-                case END_TAG:
-                case END_DOCUMENT:
-                    return token;
-                case TEXT:
-                    consumeAdditionalText();
-                    // Per interface docs, empty text regions are skipped
-                    if (mCurrentText == null || mCurrentText.length() == 0) {
-                        continue;
-                    } else {
-                        return TEXT;
-                    }
-            }
-        }
-    }
-
-    @Override
-    public int nextToken() throws XmlPullParserException, IOException {
-        if (mCurrentToken == XmlPullParser.END_TAG) {
-            mCurrentDepth--;
-        }
-
-        int token;
-        try {
-            token = peekNextExternalToken();
-            consumeToken();
-        } catch (EOFException e) {
-            token = END_DOCUMENT;
-        }
-        switch (token) {
-            case XmlPullParser.START_TAG:
-                // We need to peek forward to find the next external token so
-                // that we parse all pending INTERNAL_ATTRIBUTE tokens
-                peekNextExternalToken();
-                mCurrentDepth++;
-                break;
-        }
-        mCurrentToken = token;
-        return token;
-    }
-
-    /**
-     * Peek at the next "external" token without consuming it.
-     * <p>
-     * External tokens, such as {@link #START_TAG}, are expected by typical
-     * {@link XmlPullParser} clients. In contrast, internal tokens, such as
-     * {@link #ATTRIBUTE}, are not expected by typical clients.
-     * <p>
-     * This method consumes any internal events until it reaches the next
-     * external event.
-     */
-    private int peekNextExternalToken() throws IOException, XmlPullParserException {
-        while (true) {
-            final int token = peekNextToken();
-            switch (token) {
-                case ATTRIBUTE:
-                    consumeToken();
-                    continue;
-                default:
-                    return token;
-            }
-        }
-    }
-
-    /**
-     * Peek at the next token in the underlying stream without consuming it.
-     */
-    private int peekNextToken() throws IOException {
-        return mIn.peekByte() & 0x0f;
-    }
-
-    /**
-     * Parse and consume the next token in the underlying stream.
-     */
-    private void consumeToken() throws IOException, XmlPullParserException {
-        final int event = mIn.readByte();
-        final int token = event & 0x0f;
-        final int type = event & 0xf0;
-        switch (token) {
-            case ATTRIBUTE: {
-                final Attribute attr = obtainAttribute();
-                attr.name = mIn.readInternedUTF();
-                attr.type = type;
-                switch (type) {
-                    case TYPE_NULL:
-                    case TYPE_BOOLEAN_TRUE:
-                    case TYPE_BOOLEAN_FALSE:
-                        // Nothing extra to fill in
-                        break;
-                    case TYPE_STRING:
-                        attr.valueString = mIn.readUTF();
-                        break;
-                    case TYPE_STRING_INTERNED:
-                        attr.valueString = mIn.readInternedUTF();
-                        break;
-                    case TYPE_BYTES_HEX:
-                    case TYPE_BYTES_BASE64:
-                        final int len = mIn.readUnsignedShort();
-                        final byte[] res = new byte[len];
-                        mIn.readFully(res);
-                        attr.valueBytes = res;
-                        break;
-                    case TYPE_INT:
-                    case TYPE_INT_HEX:
-                        attr.valueInt = mIn.readInt();
-                        break;
-                    case TYPE_LONG:
-                    case TYPE_LONG_HEX:
-                        attr.valueLong = mIn.readLong();
-                        break;
-                    case TYPE_FLOAT:
-                        attr.valueFloat = mIn.readFloat();
-                        break;
-                    case TYPE_DOUBLE:
-                        attr.valueDouble = mIn.readDouble();
-                        break;
-                    default:
-                        throw new IOException("Unexpected data type " + type);
-                }
-                break;
-            }
-            case XmlPullParser.START_DOCUMENT: {
-                mCurrentName = null;
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.END_DOCUMENT: {
-                mCurrentName = null;
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.START_TAG: {
-                mCurrentName = mIn.readInternedUTF();
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.END_TAG: {
-                mCurrentName = mIn.readInternedUTF();
-                mCurrentText = null;
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.TEXT:
-            case XmlPullParser.CDSECT:
-            case XmlPullParser.PROCESSING_INSTRUCTION:
-            case XmlPullParser.COMMENT:
-            case XmlPullParser.DOCDECL:
-            case XmlPullParser.IGNORABLE_WHITESPACE: {
-                mCurrentName = null;
-                mCurrentText = mIn.readUTF();
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            case XmlPullParser.ENTITY_REF: {
-                mCurrentName = mIn.readUTF();
-                mCurrentText = resolveEntity(mCurrentName);
-                if (mAttributeCount > 0) resetAttributes();
-                break;
-            }
-            default: {
-                throw new IOException("Unknown token " + token + " with type " + type);
-            }
-        }
-    }
-
-    /**
-     * When the current tag is {@link #TEXT}, consume all subsequent "text"
-     * events, as described by {@link #next}. When finished, the current event
-     * will still be {@link #TEXT}.
-     */
-    private void consumeAdditionalText() throws IOException, XmlPullParserException {
-        String combinedText = mCurrentText;
-        while (true) {
-            final int token = peekNextExternalToken();
-            switch (token) {
-                case COMMENT:
-                case PROCESSING_INSTRUCTION:
-                    // Quietly consumed
-                    consumeToken();
-                    break;
-                case TEXT:
-                case CDSECT:
-                case ENTITY_REF:
-                    // Additional text regions collected
-                    consumeToken();
-                    combinedText += mCurrentText;
-                    break;
-                default:
-                    // Next token is something non-text, so wrap things up
-                    mCurrentToken = TEXT;
-                    mCurrentName = null;
-                    mCurrentText = combinedText;
-                    return;
-            }
-        }
-    }
-
-    static @NonNull String resolveEntity(@NonNull String entity)
-            throws XmlPullParserException {
-        switch (entity) {
-            case "lt": return "<";
-            case "gt": return ">";
-            case "amp": return "&";
-            case "apos": return "'";
-            case "quot": return "\"";
-        }
-        if (entity.length() > 1 && entity.charAt(0) == '#') {
-            final char c = (char) Integer.parseInt(entity.substring(1));
-            return new String(new char[] { c });
-        }
-        throw new XmlPullParserException("Unknown entity " + entity);
-    }
-
-    @Override
-    public void require(int type, String namespace, String name)
-            throws XmlPullParserException, IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        if (mCurrentToken != type || !Objects.equals(mCurrentName, name)) {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-    }
-
-    @Override
-    public String nextText() throws XmlPullParserException, IOException {
-        if (getEventType() != START_TAG) {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-        int eventType = next();
-        if (eventType == TEXT) {
-            String result = getText();
-            eventType = next();
-            if (eventType != END_TAG) {
-                throw new XmlPullParserException(getPositionDescription());
-            }
-            return result;
-        } else if (eventType == END_TAG) {
-            return "";
-        } else {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-    }
-
-    @Override
-    public int nextTag() throws XmlPullParserException, IOException {
-        int eventType = next();
-        if (eventType == TEXT && isWhitespace()) {
-            eventType = next();
-        }
-        if (eventType != START_TAG && eventType != END_TAG) {
-            throw new XmlPullParserException(getPositionDescription());
-        }
-        return eventType;
-    }
-
-    /**
-     * Allocate and return a new {@link Attribute} associated with the tag being
-     * currently processed. This will automatically grow the internal pool as
-     * needed.
-     */
-    private @NonNull Attribute obtainAttribute() {
-        if (mAttributeCount == mAttributes.length) {
-            final int before = mAttributes.length;
-            final int after = before + (before >> 1);
-            mAttributes = Arrays.copyOf(mAttributes, after);
-            for (int i = before; i < after; i++) {
-                mAttributes[i] = new Attribute();
-            }
-        }
-        return mAttributes[mAttributeCount++];
-    }
-
-    /**
-     * Clear any {@link Attribute} instances that have been allocated by
-     * {@link #obtainAttribute()}, returning them into the pool for recycling.
-     */
-    private void resetAttributes() {
-        for (int i = 0; i < mAttributeCount; i++) {
-            mAttributes[i].reset();
-        }
-        mAttributeCount = 0;
-    }
-
-    @Override
-    public int getAttributeIndex(String namespace, String name) {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        for (int i = 0; i < mAttributeCount; i++) {
-            if (Objects.equals(mAttributes[i].name, name)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    @Override
-    public String getAttributeValue(String namespace, String name) {
-        final int index = getAttributeIndex(namespace, name);
-        if (index != -1) {
-            return mAttributes[index].getValueString();
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public String getAttributeValue(int index) {
-        return mAttributes[index].getValueString();
-    }
-
-    @Override
-    public byte[] getAttributeBytesHex(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueBytesHex();
-    }
-
-    @Override
-    public byte[] getAttributeBytesBase64(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueBytesBase64();
-    }
-
-    @Override
-    public int getAttributeInt(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueInt();
-    }
-
-    @Override
-    public int getAttributeIntHex(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueIntHex();
-    }
-
-    @Override
-    public long getAttributeLong(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueLong();
-    }
-
-    @Override
-    public long getAttributeLongHex(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueLongHex();
-    }
-
-    @Override
-    public float getAttributeFloat(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueFloat();
-    }
-
-    @Override
-    public double getAttributeDouble(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueDouble();
-    }
-
-    @Override
-    public boolean getAttributeBoolean(int index) throws XmlPullParserException {
-        return mAttributes[index].getValueBoolean();
-    }
-
-    @Override
-    public String getText() {
-        return mCurrentText;
-    }
-
-    @Override
-    public char[] getTextCharacters(int[] holderForStartAndLength) {
-        final char[] chars = mCurrentText.toCharArray();
-        holderForStartAndLength[0] = 0;
-        holderForStartAndLength[1] = chars.length;
-        return chars;
-    }
-
-    @Override
-    public String getInputEncoding() {
-        return StandardCharsets.UTF_8.name();
-    }
-
-    @Override
-    public int getDepth() {
-        return mCurrentDepth;
-    }
-
-    @Override
-    public String getPositionDescription() {
-        // Not very helpful, but it's the best information we have
-        return "Token " + mCurrentToken + " at depth " + mCurrentDepth;
-    }
-
-    @Override
-    public int getLineNumber() {
-        return -1;
-    }
-
-    @Override
-    public int getColumnNumber() {
-        return -1;
-    }
-
-    @Override
-    public boolean isWhitespace() throws XmlPullParserException {
-        switch (mCurrentToken) {
-            case IGNORABLE_WHITESPACE:
-                return true;
-            case TEXT:
-            case CDSECT:
-                return !TextUtils.isGraphic(mCurrentText);
-            default:
-                throw new XmlPullParserException("Not applicable for token " + mCurrentToken);
-        }
-    }
-
-    @Override
-    public String getNamespace() {
-        switch (mCurrentToken) {
-            case START_TAG:
-            case END_TAG:
-                // Namespaces are unsupported
-                return NO_NAMESPACE;
-            default:
-                return null;
-        }
-    }
-
-    @Override
-    public String getName() {
-        return mCurrentName;
-    }
-
-    @Override
-    public String getPrefix() {
-        // Prefixes are not supported
-        return null;
-    }
-
-    @Override
-    public boolean isEmptyElementTag() throws XmlPullParserException {
-        switch (mCurrentToken) {
-            case START_TAG:
-                try {
-                    return (peekNextExternalToken() == END_TAG);
-                } catch (IOException e) {
-                    throw new XmlPullParserException(e.toString());
-                }
-            default:
-                throw new XmlPullParserException("Not at START_TAG");
-        }
-    }
-
-    @Override
-    public int getAttributeCount() {
-        return mAttributeCount;
-    }
-
-    @Override
-    public String getAttributeNamespace(int index) {
-        // Namespaces are unsupported
-        return NO_NAMESPACE;
-    }
-
-    @Override
-    public String getAttributeName(int index) {
-        return mAttributes[index].name;
-    }
-
-    @Override
-    public String getAttributePrefix(int index) {
-        // Prefixes are not supported
-        return null;
-    }
-
-    @Override
-    public String getAttributeType(int index) {
-        // Validation is not supported
-        return "CDATA";
-    }
-
-    @Override
-    public boolean isAttributeDefault(int index) {
-        // Validation is not supported
-        return false;
-    }
-
-    @Override
-    public int getEventType() throws XmlPullParserException {
-        return mCurrentToken;
-    }
-
-    @Override
-    public int getNamespaceCount(int depth) throws XmlPullParserException {
-        // Namespaces are unsupported
-        return 0;
-    }
-
-    @Override
-    public String getNamespacePrefix(int pos) throws XmlPullParserException {
-        // Namespaces are unsupported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getNamespaceUri(int pos) throws XmlPullParserException {
-        // Namespaces are unsupported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getNamespace(String prefix) {
-        // Namespaces are unsupported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void defineEntityReplacementText(String entityName, String replacementText)
-            throws XmlPullParserException {
-        // Custom entities are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setFeature(String name, boolean state) throws XmlPullParserException {
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean getFeature(String name) {
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setProperty(String name, Object value) throws XmlPullParserException {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Object getProperty(String name) {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    private static IllegalArgumentException illegalNamespace() {
-        throw new IllegalArgumentException("Namespaces are not supported");
-    }
-
-    /**
-     * Holder representing a single attribute. This design enables object
-     * recycling without resorting to autoboxing.
-     * <p>
-     * To support conversion between human-readable XML and binary XML, the
-     * various accessor methods will transparently convert from/to
-     * human-readable values when needed.
-     */
-    private static class Attribute {
-        public String name;
-        public int type;
-
-        public String valueString;
-        public byte[] valueBytes;
-        public int valueInt;
-        public long valueLong;
-        public float valueFloat;
-        public double valueDouble;
-
-        public void reset() {
-            name = null;
-            valueString = null;
-            valueBytes = null;
-        }
-
-        public @Nullable String getValueString() {
-            switch (type) {
-                case TYPE_NULL:
-                    return null;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    return valueString;
-                case TYPE_BYTES_HEX:
-                    return bytesToHexString(valueBytes);
-                case TYPE_BYTES_BASE64:
-                    return Base64.encodeToString(valueBytes, Base64.NO_WRAP);
-                case TYPE_INT:
-                    return Integer.toString(valueInt);
-                case TYPE_INT_HEX:
-                    return Integer.toString(valueInt, 16);
-                case TYPE_LONG:
-                    return Long.toString(valueLong);
-                case TYPE_LONG_HEX:
-                    return Long.toString(valueLong, 16);
-                case TYPE_FLOAT:
-                    return Float.toString(valueFloat);
-                case TYPE_DOUBLE:
-                    return Double.toString(valueDouble);
-                case TYPE_BOOLEAN_TRUE:
-                    return "true";
-                case TYPE_BOOLEAN_FALSE:
-                    return "false";
-                default:
-                    // Unknown data type; null is the best we can offer
-                    return null;
-            }
-        }
-
-        public @Nullable byte[] getValueBytesHex() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_NULL:
-                    return null;
-                case TYPE_BYTES_HEX:
-                case TYPE_BYTES_BASE64:
-                    return valueBytes;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return hexStringToBytes(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public @Nullable byte[] getValueBytesBase64() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_NULL:
-                    return null;
-                case TYPE_BYTES_HEX:
-                case TYPE_BYTES_BASE64:
-                    return valueBytes;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Base64.decode(valueString, Base64.NO_WRAP);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public int getValueInt() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_INT:
-                case TYPE_INT_HEX:
-                    return valueInt;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Integer.parseInt(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public int getValueIntHex() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_INT:
-                case TYPE_INT_HEX:
-                    return valueInt;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Integer.parseInt(valueString, 16);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public long getValueLong() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_LONG:
-                case TYPE_LONG_HEX:
-                    return valueLong;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Long.parseLong(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public long getValueLongHex() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_LONG:
-                case TYPE_LONG_HEX:
-                    return valueLong;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Long.parseLong(valueString, 16);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public float getValueFloat() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_FLOAT:
-                    return valueFloat;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Float.parseFloat(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public double getValueDouble() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_DOUBLE:
-                    return valueDouble;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    try {
-                        return Double.parseDouble(valueString);
-                    } catch (Exception e) {
-                        throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-
-        public boolean getValueBoolean() throws XmlPullParserException {
-            switch (type) {
-                case TYPE_BOOLEAN_TRUE:
-                    return true;
-                case TYPE_BOOLEAN_FALSE:
-                    return false;
-                case TYPE_STRING:
-                case TYPE_STRING_INTERNED:
-                    if ("true".equalsIgnoreCase(valueString)) {
-                        return true;
-                    } else if ("false".equalsIgnoreCase(valueString)) {
-                        return false;
-                    } else {
-                        throw new XmlPullParserException(
-                                "Invalid attribute " + name + ": " + valueString);
-                    }
-                default:
-                    throw new XmlPullParserException("Invalid conversion from " + type);
-            }
-        }
-    }
-
-    // NOTE: To support unbundled clients, we include an inlined copy
-    // of hex conversion logic from HexDump below
-    private final static char[] HEX_DIGITS =
-            { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
-
-    private static int toByte(char c) {
-        if (c >= '0' && c <= '9') return (c - '0');
-        if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
-        if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
-        throw new IllegalArgumentException("Invalid hex char '" + c + "'");
-    }
-
-    static String bytesToHexString(byte[] value) {
-        final int length = value.length;
-        final char[] buf = new char[length * 2];
-        int bufIndex = 0;
-        for (int i = 0; i < length; i++) {
-            byte b = value[i];
-            buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
-            buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
-        }
-        return new String(buf);
-    }
-
-    static byte[] hexStringToBytes(String value) {
-        final int length = value.length();
-        if (length % 2 != 0) {
-            throw new IllegalArgumentException("Invalid hex length " + length);
-        }
-        byte[] buffer = new byte[length / 2];
-        for (int i = 0; i < length; i += 2) {
-            buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4)
-                    | toByte(value.charAt(i + 1)));
-        }
-        return buffer;
-    }
-}
diff --git a/core/java/com/android/internal/util/BinaryXmlSerializer.java b/core/java/com/android/internal/util/BinaryXmlSerializer.java
deleted file mode 100644
index 485430a..0000000
--- a/core/java/com/android/internal/util/BinaryXmlSerializer.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import static org.xmlpull.v1.XmlPullParser.CDSECT;
-import static org.xmlpull.v1.XmlPullParser.COMMENT;
-import static org.xmlpull.v1.XmlPullParser.DOCDECL;
-import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
-import static org.xmlpull.v1.XmlPullParser.END_TAG;
-import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
-import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
-import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
-import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
-import static org.xmlpull.v1.XmlPullParser.START_TAG;
-import static org.xmlpull.v1.XmlPullParser.TEXT;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.util.TypedXmlSerializer;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-
-/**
- * Serializer that writes XML documents using a custom binary wire protocol
- * which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
- * than {@code Xml.newFastSerializer()} for a typical {@code packages.xml}.
- * <p>
- * The high-level design of the wire protocol is to directly serialize the event
- * stream, while efficiently and compactly writing strongly-typed primitives
- * delivered through the {@link TypedXmlSerializer} interface.
- * <p>
- * Each serialized event is a single byte where the lower half is a normal
- * {@link XmlPullParser} token and the upper half is an optional data type
- * signal, such as {@link #TYPE_INT}.
- * <p>
- * This serializer has some specific limitations:
- * <ul>
- * <li>Only the UTF-8 encoding is supported.
- * <li>Variable length values, such as {@code byte[]} or {@link String}, are
- * limited to 65,535 bytes in length. Note that {@link String} values are stored
- * as UTF-8 on the wire.
- * <li>Namespaces, prefixes, properties, and options are unsupported.
- * </ul>
- */
-public final class BinaryXmlSerializer implements TypedXmlSerializer {
-    /**
-     * The wire protocol always begins with a well-known magic value of
-     * {@code ABX_}, representing "Android Binary XML." The final byte is a
-     * version number which may be incremented as the protocol changes.
-     */
-    public static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[] { 0x41, 0x42, 0x58, 0x00 };
-
-    /**
-     * Internal token which represents an attribute associated with the most
-     * recent {@link #START_TAG} token.
-     */
-    static final int ATTRIBUTE = 15;
-
-    static final int TYPE_NULL = 1 << 4;
-    static final int TYPE_STRING = 2 << 4;
-    static final int TYPE_STRING_INTERNED = 3 << 4;
-    static final int TYPE_BYTES_HEX = 4 << 4;
-    static final int TYPE_BYTES_BASE64 = 5 << 4;
-    static final int TYPE_INT = 6 << 4;
-    static final int TYPE_INT_HEX = 7 << 4;
-    static final int TYPE_LONG = 8 << 4;
-    static final int TYPE_LONG_HEX = 9 << 4;
-    static final int TYPE_FLOAT = 10 << 4;
-    static final int TYPE_DOUBLE = 11 << 4;
-    static final int TYPE_BOOLEAN_TRUE = 12 << 4;
-    static final int TYPE_BOOLEAN_FALSE = 13 << 4;
-
-    private FastDataOutput mOut;
-
-    /**
-     * Stack of tags which are currently active via {@link #startTag} and which
-     * haven't been terminated via {@link #endTag}.
-     */
-    private int mTagCount = 0;
-    private String[] mTagNames;
-
-    /**
-     * Write the given token and optional {@link String} into our buffer.
-     */
-    private void writeToken(int token, @Nullable String text) throws IOException {
-        if (text != null) {
-            mOut.writeByte(token | TYPE_STRING);
-            mOut.writeUTF(text);
-        } else {
-            mOut.writeByte(token | TYPE_NULL);
-        }
-    }
-
-    @Override
-    public void setOutput(@NonNull OutputStream os, @Nullable String encoding) throws IOException {
-        if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
-            throw new UnsupportedOperationException();
-        }
-
-        mOut = FastDataOutput.obtainUsing4ByteSequences(os);
-        mOut.write(PROTOCOL_MAGIC_VERSION_0);
-
-        mTagCount = 0;
-        mTagNames = new String[8];
-    }
-
-    @Override
-    public void setOutput(Writer writer) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void flush() throws IOException {
-        if (mOut != null) {
-            mOut.flush();
-        }
-    }
-
-    @Override
-    public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
-            throws IOException {
-        if (encoding != null && !StandardCharsets.UTF_8.name().equalsIgnoreCase(encoding)) {
-            throw new UnsupportedOperationException();
-        }
-        if (standalone != null && !standalone) {
-            throw new UnsupportedOperationException();
-        }
-        mOut.writeByte(START_DOCUMENT | TYPE_NULL);
-    }
-
-    @Override
-    public void endDocument() throws IOException {
-        mOut.writeByte(END_DOCUMENT | TYPE_NULL);
-        flush();
-
-        mOut.release();
-        mOut = null;
-    }
-
-    @Override
-    public int getDepth() {
-        return mTagCount;
-    }
-
-    @Override
-    public String getNamespace() {
-        // Namespaces are unsupported
-        return XmlPullParser.NO_NAMESPACE;
-    }
-
-    @Override
-    public String getName() {
-        return mTagNames[mTagCount - 1];
-    }
-
-    @Override
-    public XmlSerializer startTag(String namespace, String name) throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        if (mTagCount == mTagNames.length) {
-            mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
-        }
-        mTagNames[mTagCount++] = name;
-        mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
-        mOut.writeInternedUTF(name);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer endTag(String namespace, String name) throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mTagCount--;
-        mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
-        mOut.writeInternedUTF(name);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_STRING);
-        mOut.writeInternedUTF(name);
-        mOut.writeUTF(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeInterned(String namespace, String name, String value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
-        mOut.writeInternedUTF(name);
-        mOut.writeInternedUTF(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
-        mOut.writeInternedUTF(name);
-        mOut.writeShort(value.length);
-        mOut.write(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
-        mOut.writeInternedUTF(name);
-        mOut.writeShort(value.length);
-        mOut.write(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeInt(String namespace, String name, int value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_INT);
-        mOut.writeInternedUTF(name);
-        mOut.writeInt(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeIntHex(String namespace, String name, int value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
-        mOut.writeInternedUTF(name);
-        mOut.writeInt(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeLong(String namespace, String name, long value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_LONG);
-        mOut.writeInternedUTF(name);
-        mOut.writeLong(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeLongHex(String namespace, String name, long value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
-        mOut.writeInternedUTF(name);
-        mOut.writeLong(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeFloat(String namespace, String name, float value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
-        mOut.writeInternedUTF(name);
-        mOut.writeFloat(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeDouble(String namespace, String name, double value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
-        mOut.writeInternedUTF(name);
-        mOut.writeDouble(value);
-        return this;
-    }
-
-    @Override
-    public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
-            throws IOException {
-        if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
-        if (value) {
-            mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
-            mOut.writeInternedUTF(name);
-        } else {
-            mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
-            mOut.writeInternedUTF(name);
-        }
-        return this;
-    }
-
-    @Override
-    public XmlSerializer text(char[] buf, int start, int len) throws IOException {
-        writeToken(TEXT, new String(buf, start, len));
-        return this;
-    }
-
-    @Override
-    public XmlSerializer text(String text) throws IOException {
-        writeToken(TEXT, text);
-        return this;
-    }
-
-    @Override
-    public void cdsect(String text) throws IOException {
-        writeToken(CDSECT, text);
-    }
-
-    @Override
-    public void entityRef(String text) throws IOException {
-        writeToken(ENTITY_REF, text);
-    }
-
-    @Override
-    public void processingInstruction(String text) throws IOException {
-        writeToken(PROCESSING_INSTRUCTION, text);
-    }
-
-    @Override
-    public void comment(String text) throws IOException {
-        writeToken(COMMENT, text);
-    }
-
-    @Override
-    public void docdecl(String text) throws IOException {
-        writeToken(DOCDECL, text);
-    }
-
-    @Override
-    public void ignorableWhitespace(String text) throws IOException {
-        writeToken(IGNORABLE_WHITESPACE, text);
-    }
-
-    @Override
-    public void setFeature(String name, boolean state) {
-        // Quietly handle no-op features
-        if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
-            return;
-        }
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean getFeature(String name) {
-        // Features are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setProperty(String name, Object value) {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Object getProperty(String name) {
-        // Properties are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void setPrefix(String prefix, String namespace) {
-        // Prefixes are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String getPrefix(String namespace, boolean generatePrefix) {
-        // Prefixes are not supported
-        throw new UnsupportedOperationException();
-    }
-
-    private static IllegalArgumentException illegalNamespace() {
-        throw new IllegalArgumentException("Namespaces are not supported");
-    }
-}
diff --git a/core/java/com/android/internal/util/FastDataInput.java b/core/java/com/android/internal/util/FastDataInput.java
deleted file mode 100644
index 5117034..0000000
--- a/core/java/com/android/internal/util/FastDataInput.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import android.annotation.NonNull;
-import android.util.CharsetUtils;
-
-import dalvik.system.VMRuntime;
-
-import java.io.BufferedInputStream;
-import java.io.Closeable;
-import java.io.DataInput;
-import java.io.DataInputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Optimized implementation of {@link DataInput} which buffers data in memory
- * from the underlying {@link InputStream}.
- * <p>
- * Benchmarks have demonstrated this class is 3x more efficient than using a
- * {@link DataInputStream} with a {@link BufferedInputStream}.
- */
-public class FastDataInput implements DataInput, Closeable {
-    private static final int MAX_UNSIGNED_SHORT = 65_535;
-
-    private static final int DEFAULT_BUFFER_SIZE = 32_768;
-
-    private static AtomicReference<FastDataInput> sInCache = new AtomicReference<>();
-
-    private final VMRuntime mRuntime;
-
-    private final byte[] mBuffer;
-    private final long mBufferPtr;
-    private final int mBufferCap;
-    private final boolean mUse4ByteSequence;
-
-    private InputStream mIn;
-    private int mBufferPos;
-    private int mBufferLim;
-
-    /**
-     * Values that have been "interned" by {@link #readInternedUTF()}.
-     */
-    private int mStringRefCount = 0;
-    private String[] mStringRefs = new String[32];
-
-    /**
-     * @deprecated callers must specify {@code use4ByteSequence} so they make a
-     *             clear choice about working around a long-standing ART bug, as
-     *             described by the {@code kUtfUse4ByteSequence} comments in
-     *             {@code art/runtime/jni/jni_internal.cc}.
-     */
-    @Deprecated
-    public FastDataInput(@NonNull InputStream in, int bufferSize) {
-        this(in, bufferSize, true /* use4ByteSequence */);
-    }
-
-    public FastDataInput(@NonNull InputStream in, int bufferSize, boolean use4ByteSequence) {
-        mRuntime = VMRuntime.getRuntime();
-        mIn = Objects.requireNonNull(in);
-        if (bufferSize < 8) {
-            throw new IllegalArgumentException();
-        }
-
-        mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize);
-        mBufferPtr = mRuntime.addressOf(mBuffer);
-        mBufferCap = mBuffer.length;
-        mUse4ByteSequence = use4ByteSequence;
-    }
-
-    /**
-     * Obtain a {@link FastDataInput} configured with the given
-     * {@link InputStream} and which encodes large code-points using 3-byte
-     * sequences.
-     * <p>
-     * This <em>is</em> compatible with the {@link DataInput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataInput obtainUsing3ByteSequences(@NonNull InputStream in) {
-        return new FastDataInput(in, DEFAULT_BUFFER_SIZE, false /* use4ByteSequence */);
-    }
-
-    /**
-     * Obtain a {@link FastDataInput} configured with the given
-     * {@link InputStream} and which decodes large code-points using 4-byte
-     * sequences.
-     * <p>
-     * This <em>is not</em> compatible with the {@link DataInput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataInput obtainUsing4ByteSequences(@NonNull InputStream in) {
-        FastDataInput instance = sInCache.getAndSet(null);
-        if (instance != null) {
-            instance.setInput(in);
-            return instance;
-        }
-        return new FastDataInput(in, DEFAULT_BUFFER_SIZE, true /* use4ByteSequence */);
-    }
-
-    /**
-     * Release a {@link FastDataInput} to potentially be recycled. You must not
-     * interact with the object after releasing it.
-     */
-    public void release() {
-        mIn = null;
-        mBufferPos = 0;
-        mBufferLim = 0;
-        mStringRefCount = 0;
-
-        if (mBufferCap == DEFAULT_BUFFER_SIZE && mUse4ByteSequence) {
-            // Try to return to the cache.
-            sInCache.compareAndSet(null, this);
-        }
-    }
-
-    /**
-     * Re-initializes the object for the new input.
-     */
-    private void setInput(@NonNull InputStream in) {
-        mIn = Objects.requireNonNull(in);
-        mBufferPos = 0;
-        mBufferLim = 0;
-        mStringRefCount = 0;
-    }
-
-    private void fill(int need) throws IOException {
-        final int remain = mBufferLim - mBufferPos;
-        System.arraycopy(mBuffer, mBufferPos, mBuffer, 0, remain);
-        mBufferPos = 0;
-        mBufferLim = remain;
-        need -= remain;
-
-        while (need > 0) {
-            int c = mIn.read(mBuffer, mBufferLim, mBufferCap - mBufferLim);
-            if (c == -1) {
-                throw new EOFException();
-            } else {
-                mBufferLim += c;
-                need -= c;
-            }
-        }
-    }
-
-    @Override
-    public void close() throws IOException {
-        mIn.close();
-        release();
-    }
-
-    @Override
-    public void readFully(byte[] b) throws IOException {
-        readFully(b, 0, b.length);
-    }
-
-    @Override
-    public void readFully(byte[] b, int off, int len) throws IOException {
-        // Attempt to read directly from buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        if (mBufferCap >= len) {
-            if (mBufferLim - mBufferPos < len) fill(len);
-            System.arraycopy(mBuffer, mBufferPos, b, off, len);
-            mBufferPos += len;
-        } else {
-            final int remain = mBufferLim - mBufferPos;
-            System.arraycopy(mBuffer, mBufferPos, b, off, remain);
-            mBufferPos += remain;
-            off += remain;
-            len -= remain;
-
-            while (len > 0) {
-                int c = mIn.read(b, off, len);
-                if (c == -1) {
-                    throw new EOFException();
-                } else {
-                    off += c;
-                    len -= c;
-                }
-            }
-        }
-    }
-
-    @Override
-    public String readUTF() throws IOException {
-        if (mUse4ByteSequence) {
-            return readUTFUsing4ByteSequences();
-        } else {
-            return readUTFUsing3ByteSequences();
-        }
-    }
-
-    private String readUTFUsing4ByteSequences() throws IOException {
-        // Attempt to read directly from buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        final int len = readUnsignedShort();
-        if (mBufferCap > len) {
-            if (mBufferLim - mBufferPos < len) fill(len);
-            final String res = CharsetUtils.fromModifiedUtf8Bytes(mBufferPtr, mBufferPos, len);
-            mBufferPos += len;
-            return res;
-        } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            readFully(tmp, 0, len);
-            return CharsetUtils.fromModifiedUtf8Bytes(mRuntime.addressOf(tmp), 0, len);
-        }
-    }
-
-    private String readUTFUsing3ByteSequences() throws IOException {
-        // Attempt to read directly from buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        final int len = readUnsignedShort();
-        if (mBufferCap > len) {
-            if (mBufferLim - mBufferPos < len) fill(len);
-            final String res = ModifiedUtf8.decode(mBuffer, new char[len], mBufferPos, len);
-            mBufferPos += len;
-            return res;
-        } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            readFully(tmp, 0, len);
-            return ModifiedUtf8.decode(tmp, new char[len], 0, len);
-        }
-    }
-
-    /**
-     * Read a {@link String} value with the additional signal that the given
-     * value is a candidate for being canonicalized, similar to
-     * {@link String#intern()}.
-     * <p>
-     * Canonicalization is implemented by writing each unique string value once
-     * the first time it appears, and then writing a lightweight {@code short}
-     * reference when that string is written again in the future.
-     *
-     * @see FastDataOutput#writeInternedUTF(String)
-     */
-    public @NonNull String readInternedUTF() throws IOException {
-        final int ref = readUnsignedShort();
-        if (ref == MAX_UNSIGNED_SHORT) {
-            final String s = readUTF();
-
-            // We can only safely intern when we have remaining values; if we're
-            // full we at least sent the string value above
-            if (mStringRefCount < MAX_UNSIGNED_SHORT) {
-                if (mStringRefCount == mStringRefs.length) {
-                    mStringRefs = Arrays.copyOf(mStringRefs,
-                            mStringRefCount + (mStringRefCount >> 1));
-                }
-                mStringRefs[mStringRefCount++] = s;
-            }
-
-            return s;
-        } else {
-            return mStringRefs[ref];
-        }
-    }
-
-    @Override
-    public boolean readBoolean() throws IOException {
-        return readByte() != 0;
-    }
-
-    /**
-     * Returns the same decoded value as {@link #readByte()} but without
-     * actually consuming the underlying data.
-     */
-    public byte peekByte() throws IOException {
-        if (mBufferLim - mBufferPos < 1) fill(1);
-        return mBuffer[mBufferPos];
-    }
-
-    @Override
-    public byte readByte() throws IOException {
-        if (mBufferLim - mBufferPos < 1) fill(1);
-        return mBuffer[mBufferPos++];
-    }
-
-    @Override
-    public int readUnsignedByte() throws IOException {
-        return Byte.toUnsignedInt(readByte());
-    }
-
-    @Override
-    public short readShort() throws IOException {
-        if (mBufferLim - mBufferPos < 2) fill(2);
-        return (short) (((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                        ((mBuffer[mBufferPos++] & 0xff) <<  0));
-    }
-
-    @Override
-    public int readUnsignedShort() throws IOException {
-        return Short.toUnsignedInt((short) readShort());
-    }
-
-    @Override
-    public char readChar() throws IOException {
-        return (char) readShort();
-    }
-
-    @Override
-    public int readInt() throws IOException {
-        if (mBufferLim - mBufferPos < 4) fill(4);
-        return (((mBuffer[mBufferPos++] & 0xff) << 24) |
-                ((mBuffer[mBufferPos++] & 0xff) << 16) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  0));
-    }
-
-    @Override
-    public long readLong() throws IOException {
-        if (mBufferLim - mBufferPos < 8) fill(8);
-        int h = ((mBuffer[mBufferPos++] & 0xff) << 24) |
-                ((mBuffer[mBufferPos++] & 0xff) << 16) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  0);
-        int l = ((mBuffer[mBufferPos++] & 0xff) << 24) |
-                ((mBuffer[mBufferPos++] & 0xff) << 16) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
-                ((mBuffer[mBufferPos++] & 0xff) <<  0);
-        return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
-    }
-
-    @Override
-    public float readFloat() throws IOException {
-        return Float.intBitsToFloat(readInt());
-    }
-
-    @Override
-    public double readDouble() throws IOException {
-        return Double.longBitsToDouble(readLong());
-    }
-
-    @Override
-    public int skipBytes(int n) throws IOException {
-        // Callers should read data piecemeal
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String readLine() throws IOException {
-        // Callers should read data piecemeal
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/core/java/com/android/internal/util/FastDataOutput.java b/core/java/com/android/internal/util/FastDataOutput.java
deleted file mode 100644
index 5b6075e..0000000
--- a/core/java/com/android/internal/util/FastDataOutput.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.util;
-
-import android.annotation.NonNull;
-import android.util.CharsetUtils;
-
-import dalvik.system.VMRuntime;
-
-import java.io.BufferedOutputStream;
-import java.io.Closeable;
-import java.io.DataOutput;
-import java.io.DataOutputStream;
-import java.io.Flushable;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Optimized implementation of {@link DataOutput} which buffers data in memory
- * before flushing to the underlying {@link OutputStream}.
- * <p>
- * Benchmarks have demonstrated this class is 2x more efficient than using a
- * {@link DataOutputStream} with a {@link BufferedOutputStream}.
- */
-public class FastDataOutput implements DataOutput, Flushable, Closeable {
-    private static final int MAX_UNSIGNED_SHORT = 65_535;
-
-    private static final int DEFAULT_BUFFER_SIZE = 32_768;
-
-    private static AtomicReference<FastDataOutput> sOutCache = new AtomicReference<>();
-
-    private final VMRuntime mRuntime;
-
-    private final byte[] mBuffer;
-    private final long mBufferPtr;
-    private final int mBufferCap;
-    private final boolean mUse4ByteSequence;
-
-    private OutputStream mOut;
-    private int mBufferPos;
-
-    /**
-     * Values that have been "interned" by {@link #writeInternedUTF(String)}.
-     */
-    private final HashMap<String, Integer> mStringRefs = new HashMap<>();
-
-    /**
-     * @deprecated callers must specify {@code use4ByteSequence} so they make a
-     *             clear choice about working around a long-standing ART bug, as
-     *             described by the {@code kUtfUse4ByteSequence} comments in
-     *             {@code art/runtime/jni/jni_internal.cc}.
-     */
-    @Deprecated
-    public FastDataOutput(@NonNull OutputStream out, int bufferSize) {
-        this(out, bufferSize, true /* use4ByteSequence */);
-    }
-
-    public FastDataOutput(@NonNull OutputStream out, int bufferSize, boolean use4ByteSequence) {
-        mRuntime = VMRuntime.getRuntime();
-        if (bufferSize < 8) {
-            throw new IllegalArgumentException();
-        }
-
-        mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize);
-        mBufferPtr = mRuntime.addressOf(mBuffer);
-        mBufferCap = mBuffer.length;
-        mUse4ByteSequence = use4ByteSequence;
-
-        setOutput(out);
-    }
-
-    /**
-     * Obtain a {@link FastDataOutput} configured with the given
-     * {@link OutputStream} and which encodes large code-points using 3-byte
-     * sequences.
-     * <p>
-     * This <em>is</em> compatible with the {@link DataOutput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataOutput obtainUsing3ByteSequences(@NonNull OutputStream out) {
-        return new FastDataOutput(out, DEFAULT_BUFFER_SIZE, false /* use4ByteSequence */);
-    }
-
-    /**
-     * Obtain a {@link FastDataOutput} configured with the given
-     * {@link OutputStream} and which encodes large code-points using 4-byte
-     * sequences.
-     * <p>
-     * This <em>is not</em> compatible with the {@link DataOutput} API contract,
-     * which specifies that large code-points must be encoded with 3-byte
-     * sequences.
-     */
-    public static FastDataOutput obtainUsing4ByteSequences(@NonNull OutputStream out) {
-        FastDataOutput instance = sOutCache.getAndSet(null);
-        if (instance != null) {
-            instance.setOutput(out);
-            return instance;
-        }
-        return new FastDataOutput(out, DEFAULT_BUFFER_SIZE, true /* use4ByteSequence */);
-    }
-
-    /**
-     * Release a {@link FastDataOutput} to potentially be recycled. You must not
-     * interact with the object after releasing it.
-     */
-    public void release() {
-        if (mBufferPos > 0) {
-            throw new IllegalStateException("Lingering data, call flush() before releasing.");
-        }
-
-        mOut = null;
-        mBufferPos = 0;
-        mStringRefs.clear();
-
-        if (mBufferCap == DEFAULT_BUFFER_SIZE && mUse4ByteSequence) {
-            // Try to return to the cache.
-            sOutCache.compareAndSet(null, this);
-        }
-    }
-
-    /**
-     * Re-initializes the object for the new output.
-     */
-    private void setOutput(@NonNull OutputStream out) {
-        mOut = Objects.requireNonNull(out);
-        mBufferPos = 0;
-        mStringRefs.clear();
-    }
-
-    private void drain() throws IOException {
-        if (mBufferPos > 0) {
-            mOut.write(mBuffer, 0, mBufferPos);
-            mBufferPos = 0;
-        }
-    }
-
-    @Override
-    public void flush() throws IOException {
-        drain();
-        mOut.flush();
-    }
-
-    @Override
-    public void close() throws IOException {
-        mOut.close();
-        release();
-    }
-
-    @Override
-    public void write(int b) throws IOException {
-        writeByte(b);
-    }
-
-    @Override
-    public void write(byte[] b) throws IOException {
-        write(b, 0, b.length);
-    }
-
-    @Override
-    public void write(byte[] b, int off, int len) throws IOException {
-        if (mBufferCap < len) {
-            drain();
-            mOut.write(b, off, len);
-        } else {
-            if (mBufferCap - mBufferPos < len) drain();
-            System.arraycopy(b, off, mBuffer, mBufferPos, len);
-            mBufferPos += len;
-        }
-    }
-
-    @Override
-    public void writeUTF(String s) throws IOException {
-        if (mUse4ByteSequence) {
-            writeUTFUsing4ByteSequences(s);
-        } else {
-            writeUTFUsing3ByteSequences(s);
-        }
-    }
-
-    private void writeUTFUsing4ByteSequences(String s) throws IOException {
-        // Attempt to write directly to buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        if (mBufferCap - mBufferPos < 2 + s.length()) drain();
-
-        // Magnitude of this returned value indicates the number of bytes
-        // required to encode the string; sign indicates success/failure
-        int len = CharsetUtils.toModifiedUtf8Bytes(s, mBufferPtr, mBufferPos + 2, mBufferCap);
-        if (Math.abs(len) > MAX_UNSIGNED_SHORT) {
-            throw new IOException("Modified UTF-8 length too large: " + len);
-        }
-
-        if (len >= 0) {
-            // Positive value indicates the string was encoded into the buffer
-            // successfully, so we only need to prefix with length
-            writeShort(len);
-            mBufferPos += len;
-        } else {
-            // Negative value indicates buffer was too small and we need to
-            // allocate a temporary buffer for encoding
-            len = -len;
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
-            writeShort(len);
-            write(tmp, 0, len);
-        }
-    }
-
-    private void writeUTFUsing3ByteSequences(String s) throws IOException {
-        final int len = (int) ModifiedUtf8.countBytes(s, false);
-        if (len > MAX_UNSIGNED_SHORT) {
-            throw new IOException("Modified UTF-8 length too large: " + len);
-        }
-
-        // Attempt to write directly to buffer space if there's enough room,
-        // otherwise fall back to chunking into place
-        if (mBufferCap >= 2 + len) {
-            if (mBufferCap - mBufferPos < 2 + len) drain();
-            writeShort(len);
-            ModifiedUtf8.encode(mBuffer, mBufferPos, s);
-            mBufferPos += len;
-        } else {
-            final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
-            ModifiedUtf8.encode(tmp, 0, s);
-            writeShort(len);
-            write(tmp, 0, len);
-        }
-    }
-
-    /**
-     * Write a {@link String} value with the additional signal that the given
-     * value is a candidate for being canonicalized, similar to
-     * {@link String#intern()}.
-     * <p>
-     * Canonicalization is implemented by writing each unique string value once
-     * the first time it appears, and then writing a lightweight {@code short}
-     * reference when that string is written again in the future.
-     *
-     * @see FastDataInput#readInternedUTF()
-     */
-    public void writeInternedUTF(@NonNull String s) throws IOException {
-        Integer ref = mStringRefs.get(s);
-        if (ref != null) {
-            writeShort(ref);
-        } else {
-            writeShort(MAX_UNSIGNED_SHORT);
-            writeUTF(s);
-
-            // We can only safely intern when we have remaining values; if we're
-            // full we at least sent the string value above
-            ref = mStringRefs.size();
-            if (ref < MAX_UNSIGNED_SHORT) {
-                mStringRefs.put(s, ref);
-            }
-        }
-    }
-
-    @Override
-    public void writeBoolean(boolean v) throws IOException {
-        writeByte(v ? 1 : 0);
-    }
-
-    @Override
-    public void writeByte(int v) throws IOException {
-        if (mBufferCap - mBufferPos < 1) drain();
-        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeShort(int v) throws IOException {
-        if (mBufferCap - mBufferPos < 2) drain();
-        mBuffer[mBufferPos++] = (byte) ((v >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeChar(int v) throws IOException {
-        writeShort((short) v);
-    }
-
-    @Override
-    public void writeInt(int v) throws IOException {
-        if (mBufferCap - mBufferPos < 4) drain();
-        mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeLong(long v) throws IOException {
-        if (mBufferCap - mBufferPos < 8) drain();
-        int i = (int) (v >> 32);
-        mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  0) & 0xff);
-        i = (int) v;
-        mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  8) & 0xff);
-        mBuffer[mBufferPos++] = (byte) ((i >>  0) & 0xff);
-    }
-
-    @Override
-    public void writeFloat(float v) throws IOException {
-        writeInt(Float.floatToIntBits(v));
-    }
-
-    @Override
-    public void writeDouble(double v) throws IOException {
-        writeLong(Double.doubleToLongBits(v));
-    }
-
-    @Override
-    public void writeBytes(String s) throws IOException {
-        // Callers should use writeUTF()
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void writeChars(String s) throws IOException {
-        // Callers should use writeUTF()
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/core/java/com/android/internal/util/ModifiedUtf8.java b/core/java/com/android/internal/util/ModifiedUtf8.java
deleted file mode 100644
index a144c00..0000000
--- a/core/java/com/android/internal/util/ModifiedUtf8.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You 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.io.UTFDataFormatException;
-
-public class ModifiedUtf8 {
-    /**
-     * Decodes a byte array containing <i>modified UTF-8</i> bytes into a string.
-     *
-     * <p>Note that although this method decodes the (supposedly impossible) zero byte to U+0000,
-     * that's what the RI does too.
-     */
-    public static String decode(byte[] in, char[] out, int offset, int utfSize)
-            throws UTFDataFormatException {
-        int count = 0, s = 0, a;
-        while (count < utfSize) {
-            if ((out[s] = (char) in[offset + count++]) < '\u0080') {
-                s++;
-            } else if (((a = out[s]) & 0xe0) == 0xc0) {
-                if (count >= utfSize) {
-                    throw new UTFDataFormatException("bad second byte at " + count);
-                }
-                int b = in[offset + count++];
-                if ((b & 0xC0) != 0x80) {
-                    throw new UTFDataFormatException("bad second byte at " + (count - 1));
-                }
-                out[s++] = (char) (((a & 0x1F) << 6) | (b & 0x3F));
-            } else if ((a & 0xf0) == 0xe0) {
-                if (count + 1 >= utfSize) {
-                    throw new UTFDataFormatException("bad third byte at " + (count + 1));
-                }
-                int b = in[offset + count++];
-                int c = in[offset + count++];
-                if (((b & 0xC0) != 0x80) || ((c & 0xC0) != 0x80)) {
-                    throw new UTFDataFormatException("bad second or third byte at " + (count - 2));
-                }
-                out[s++] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F));
-            } else {
-                throw new UTFDataFormatException("bad byte at " + (count - 1));
-            }
-        }
-        return new String(out, 0, s);
-    }
-
-    /**
-     * Returns the number of bytes the modified UTF-8 representation of 's' would take. Note
-     * that this is just the space for the bytes representing the characters, not the length
-     * which precedes those bytes, because different callers represent the length differently,
-     * as two, four, or even eight bytes. If {@code shortLength} is true, we'll throw an
-     * exception if the string is too long for its length to be represented by a short.
-     */
-    public static long countBytes(String s, boolean shortLength) throws UTFDataFormatException {
-        long result = 0;
-        final int length = s.length();
-        for (int i = 0; i < length; ++i) {
-            char ch = s.charAt(i);
-            if (ch != 0 && ch <= 127) { // U+0000 uses two bytes.
-                ++result;
-            } else if (ch <= 2047) {
-                result += 2;
-            } else {
-                result += 3;
-            }
-            if (shortLength && result > 65535) {
-                throw new UTFDataFormatException("String more than 65535 UTF bytes long");
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Encodes the <i>modified UTF-8</i> bytes corresponding to string {@code s} into the
-     * byte array {@code dst}, starting at the given {@code offset}.
-     */
-    public static void encode(byte[] dst, int offset, String s) {
-        final int length = s.length();
-        for (int i = 0; i < length; i++) {
-            char ch = s.charAt(i);
-            if (ch != 0 && ch <= 127) { // U+0000 uses two bytes.
-                dst[offset++] = (byte) ch;
-            } else if (ch <= 2047) {
-                dst[offset++] = (byte) (0xc0 | (0x1f & (ch >> 6)));
-                dst[offset++] = (byte) (0x80 | (0x3f & ch));
-            } else {
-                dst[offset++] = (byte) (0xe0 | (0x0f & (ch >> 12)));
-                dst[offset++] = (byte) (0x80 | (0x3f & (ch >> 6)));
-                dst[offset++] = (byte) (0x80 | (0x3f & ch));
-            }
-        }
-    }
-
-    private ModifiedUtf8() {
-    }
-}
diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java
index de6b65f3..af5e3b3 100644
--- a/core/java/com/android/internal/util/XmlUtils.java
+++ b/core/java/com/android/internal/util/XmlUtils.java
@@ -25,10 +25,11 @@
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Base64;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import libcore.util.HexEncoding;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index 1520ea5..0381510 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -71,10 +71,12 @@
         }
     }
 
-    void onTransactionHang(bool isGpuHang) {
+    void onTransactionHang(const std::string& reason) {
         if (mTransactionHangObject) {
+            JNIEnv* env = getenv(mVm);
+            ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str()));
             getenv(mVm)->CallVoidMethod(mTransactionHangObject,
-                                        gTransactionHangCallback.onTransactionHang, isGpuHang);
+                                        gTransactionHangCallback.onTransactionHang, jReason.get());
         }
     }
 
@@ -177,7 +179,7 @@
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
     return queue->isSameSurfaceControl(reinterpret_cast<SurfaceControl*>(surfaceControl));
 }
-  
+
 static void nativeSetTransactionHangCallback(JNIEnv* env, jclass clazz, jlong ptr,
                                              jobject transactionHangCallback) {
     sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(ptr);
@@ -186,9 +188,8 @@
     } else {
         sp<TransactionHangCallbackWrapper> wrapper =
                 new TransactionHangCallbackWrapper{env, transactionHangCallback};
-        queue->setTransactionHangCallback([wrapper](bool isGpuHang) {
-            wrapper->onTransactionHang(isGpuHang);
-        });
+        queue->setTransactionHangCallback(
+                [wrapper](const std::string& reason) { wrapper->onTransactionHang(reason); });
     }
 }
 
@@ -236,7 +237,8 @@
     jclass transactionHangClass =
             FindClassOrDie(env, "android/graphics/BLASTBufferQueue$TransactionHangCallback");
     gTransactionHangCallback.onTransactionHang =
-            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang", "(Z)V");
+            GetMethodIDOrDie(env, transactionHangClass, "onTransactionHang",
+                             "(Ljava/lang/String;)V");
 
     return 0;
 }
diff --git a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
index 09f3a72..2437a51 100644
--- a/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
+++ b/core/jni/android_hardware_camera2_utils_SurfaceUtils.cpp
@@ -89,6 +89,24 @@
 
 extern "C" {
 
+static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) {
+    ALOGV("nativeDetectSurfaceType");
+    sp<ANativeWindow> anw;
+    if ((anw = getNativeWindow(env, surface)) == NULL) {
+        ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__);
+        return BAD_VALUE;
+    }
+    int32_t fmt = 0;
+    status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &fmt);
+    if (err != NO_ERROR) {
+        ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__,
+              strerror(-err), err);
+        OVERRIDE_SURFACE_ERROR(err);
+        return err;
+    }
+    return fmt;
+}
+
 static jint SurfaceUtils_nativeDetectSurfaceDataspace(JNIEnv* env, jobject thiz, jobject surface) {
     ALOGV("nativeDetectSurfaceDataspace");
     sp<ANativeWindow> anw;
@@ -107,27 +125,6 @@
     return fmt;
 }
 
-static jint SurfaceUtils_nativeDetectSurfaceType(JNIEnv* env, jobject thiz, jobject surface) {
-    ALOGV("nativeDetectSurfaceType");
-    sp<ANativeWindow> anw;
-    if ((anw = getNativeWindow(env, surface)) == NULL) {
-        ALOGE("%s: Could not retrieve native window from surface.", __FUNCTION__);
-        return BAD_VALUE;
-    }
-    int32_t halFmt = 0;
-    status_t err = anw->query(anw.get(), NATIVE_WINDOW_FORMAT, &halFmt);
-    if (err != NO_ERROR) {
-        ALOGE("%s: Error while querying surface pixel format %s (%d).", __FUNCTION__,
-              strerror(-err), err);
-        OVERRIDE_SURFACE_ERROR(err);
-        return err;
-    }
-    int32_t dataspace = SurfaceUtils_nativeDetectSurfaceDataspace(env, thiz, surface);
-    int32_t fmt = static_cast<int32_t>(
-            mapHalFormatDataspaceToPublicFormat(halFmt, static_cast<android_dataspace>(dataspace)));
-    return fmt;
-}
-
 static jint SurfaceUtils_nativeDetectSurfaceDimens(JNIEnv* env, jobject thiz, jobject surface,
                                                    jintArray dimens) {
     ALOGV("nativeGetSurfaceDimens");
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index e36815c..8fd0401 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -190,12 +190,24 @@
 static struct {
     jclass clazz;
     jmethodID ctor;
+    jfieldID min;
+    jfieldID max;
+} gRefreshRateRangeClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID ctor;
+    jfieldID physical;
+    jfieldID render;
+} gRefreshRateRangesClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID ctor;
     jfieldID defaultMode;
     jfieldID allowGroupSwitching;
-    jfieldID primaryRefreshRateMin;
-    jfieldID primaryRefreshRateMax;
-    jfieldID appRequestRefreshRateMin;
-    jfieldID appRequestRefreshRateMax;
+    jfieldID primaryRanges;
+    jfieldID appRequestRanges;
 } gDesiredDisplayModeSpecsClassInfo;
 
 static struct {
@@ -1190,6 +1202,39 @@
     return object;
 }
 
+struct RefreshRateRange {
+    const float min;
+    const float max;
+
+    RefreshRateRange(float min, float max) : min(min), max(max) {}
+
+    RefreshRateRange(JNIEnv* env, jobject obj)
+          : min(env->GetFloatField(obj, gRefreshRateRangeClassInfo.min)),
+            max(env->GetFloatField(obj, gRefreshRateRangeClassInfo.max)) {}
+
+    jobject toJava(JNIEnv* env) const {
+        return env->NewObject(gRefreshRateRangeClassInfo.clazz, gRefreshRateRangeClassInfo.ctor,
+                              min, max);
+    }
+};
+
+struct RefreshRateRanges {
+    const RefreshRateRange physical;
+    const RefreshRateRange render;
+
+    RefreshRateRanges(RefreshRateRange physical, RefreshRateRange render)
+          : physical(physical), render(render) {}
+
+    RefreshRateRanges(JNIEnv* env, jobject obj)
+          : physical(env, env->GetObjectField(obj, gRefreshRateRangesClassInfo.physical)),
+            render(env, env->GetObjectField(obj, gRefreshRateRangesClassInfo.render)) {}
+
+    jobject toJava(JNIEnv* env) const {
+        return env->NewObject(gRefreshRateRangesClassInfo.clazz, gRefreshRateRangesClassInfo.ctor,
+                              physical.toJava(env), render.toJava(env));
+    }
+};
+
 static jboolean nativeSetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobject tokenObj,
                                                  jobject DesiredDisplayModeSpecs) {
     sp<IBinder> token(ibinderForJavaObject(env, tokenObj));
@@ -1200,25 +1245,23 @@
     jboolean allowGroupSwitching =
             env->GetBooleanField(DesiredDisplayModeSpecs,
                                  gDesiredDisplayModeSpecsClassInfo.allowGroupSwitching);
-    jfloat primaryRefreshRateMin =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMin);
-    jfloat primaryRefreshRateMax =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMax);
-    jfloat appRequestRefreshRateMin =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMin);
-    jfloat appRequestRefreshRateMax =
-            env->GetFloatField(DesiredDisplayModeSpecs,
-                               gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMax);
 
-    size_t result = SurfaceComposerClient::setDesiredDisplayModeSpecs(token, defaultMode,
-                                                                      allowGroupSwitching,
-                                                                      primaryRefreshRateMin,
-                                                                      primaryRefreshRateMax,
-                                                                      appRequestRefreshRateMin,
-                                                                      appRequestRefreshRateMax);
+    const jobject primaryRangesObject =
+            env->GetObjectField(DesiredDisplayModeSpecs,
+                                gDesiredDisplayModeSpecsClassInfo.primaryRanges);
+    const jobject appRequestRangesObject =
+            env->GetObjectField(DesiredDisplayModeSpecs,
+                                gDesiredDisplayModeSpecsClassInfo.appRequestRanges);
+    const RefreshRateRanges primaryRanges(env, primaryRangesObject);
+    const RefreshRateRanges appRequestRanges(env, appRequestRangesObject);
+
+    size_t result =
+            SurfaceComposerClient::setDesiredDisplayModeSpecs(token, defaultMode,
+                                                              allowGroupSwitching,
+                                                              primaryRanges.physical.min,
+                                                              primaryRanges.physical.max,
+                                                              appRequestRanges.physical.min,
+                                                              appRequestRanges.physical.max);
     return result == NO_ERROR ? JNI_TRUE : JNI_FALSE;
 }
 
@@ -1228,22 +1271,31 @@
 
     ui::DisplayModeId defaultMode;
     bool allowGroupSwitching;
-    float primaryRefreshRateMin;
-    float primaryRefreshRateMax;
-    float appRequestRefreshRateMin;
-    float appRequestRefreshRateMax;
+    float primaryPhysicalRefreshRateMin;
+    float primaryPhysicalRefreshRateMax;
+    float appRequestPhysicalRefreshRateMin;
+    float appRequestPhysicalRefreshRateMax;
     if (SurfaceComposerClient::getDesiredDisplayModeSpecs(token, &defaultMode, &allowGroupSwitching,
-                                                          &primaryRefreshRateMin,
-                                                          &primaryRefreshRateMax,
-                                                          &appRequestRefreshRateMin,
-                                                          &appRequestRefreshRateMax) != NO_ERROR) {
+                                                          &primaryPhysicalRefreshRateMin,
+                                                          &primaryPhysicalRefreshRateMax,
+                                                          &appRequestPhysicalRefreshRateMin,
+                                                          &appRequestPhysicalRefreshRateMax) !=
+        NO_ERROR) {
         return nullptr;
     }
 
+    const RefreshRateRange primaryPhysicalRange(primaryPhysicalRefreshRateMin,
+                                                primaryPhysicalRefreshRateMax);
+    const RefreshRateRange appRequestPhysicalRange(appRequestPhysicalRefreshRateMin,
+                                                   appRequestPhysicalRefreshRateMax);
+
+    // TODO(b/241460058): populate the render ranges
+    const RefreshRateRanges primaryRanges(primaryPhysicalRange, primaryPhysicalRange);
+    const RefreshRateRanges appRequestRanges(appRequestPhysicalRange, appRequestPhysicalRange);
+
     return env->NewObject(gDesiredDisplayModeSpecsClassInfo.clazz,
                           gDesiredDisplayModeSpecsClassInfo.ctor, defaultMode, allowGroupSwitching,
-                          primaryRefreshRateMin, primaryRefreshRateMax, appRequestRefreshRateMin,
-                          appRequestRefreshRateMax);
+                          primaryRanges.toJava(env), appRequestRanges.toJava(env));
 }
 
 static jobject nativeGetDisplayNativePrimaries(JNIEnv* env, jclass, jobject tokenObj) {
@@ -2235,23 +2287,45 @@
     gDisplayPrimariesClassInfo.white = GetFieldIDOrDie(env, displayPrimariesClazz, "white",
             "Landroid/view/SurfaceControl$CieXyz;");
 
+    jclass RefreshRateRangeClazz =
+            FindClassOrDie(env, "android/view/SurfaceControl$RefreshRateRange");
+    gRefreshRateRangeClassInfo.clazz = MakeGlobalRefOrDie(env, RefreshRateRangeClazz);
+    gRefreshRateRangeClassInfo.ctor =
+            GetMethodIDOrDie(env, gRefreshRateRangeClassInfo.clazz, "<init>", "(FF)V");
+    gRefreshRateRangeClassInfo.min = GetFieldIDOrDie(env, RefreshRateRangeClazz, "min", "F");
+    gRefreshRateRangeClassInfo.max = GetFieldIDOrDie(env, RefreshRateRangeClazz, "max", "F");
+
+    jclass RefreshRateRangesClazz =
+            FindClassOrDie(env, "android/view/SurfaceControl$RefreshRateRanges");
+    gRefreshRateRangesClassInfo.clazz = MakeGlobalRefOrDie(env, RefreshRateRangesClazz);
+    gRefreshRateRangesClassInfo.ctor =
+            GetMethodIDOrDie(env, gRefreshRateRangesClassInfo.clazz, "<init>",
+                             "(Landroid/view/SurfaceControl$RefreshRateRange;Landroid/view/"
+                             "SurfaceControl$RefreshRateRange;)V");
+    gRefreshRateRangesClassInfo.physical =
+            GetFieldIDOrDie(env, RefreshRateRangesClazz, "physical",
+                            "Landroid/view/SurfaceControl$RefreshRateRange;");
+    gRefreshRateRangesClassInfo.render =
+            GetFieldIDOrDie(env, RefreshRateRangesClazz, "render",
+                            "Landroid/view/SurfaceControl$RefreshRateRange;");
+
     jclass DesiredDisplayModeSpecsClazz =
             FindClassOrDie(env, "android/view/SurfaceControl$DesiredDisplayModeSpecs");
     gDesiredDisplayModeSpecsClassInfo.clazz = MakeGlobalRefOrDie(env, DesiredDisplayModeSpecsClazz);
     gDesiredDisplayModeSpecsClassInfo.ctor =
-            GetMethodIDOrDie(env, gDesiredDisplayModeSpecsClassInfo.clazz, "<init>", "(IZFFFF)V");
+            GetMethodIDOrDie(env, gDesiredDisplayModeSpecsClassInfo.clazz, "<init>",
+                             "(IZLandroid/view/SurfaceControl$RefreshRateRanges;Landroid/view/"
+                             "SurfaceControl$RefreshRateRanges;)V");
     gDesiredDisplayModeSpecsClassInfo.defaultMode =
             GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "defaultMode", "I");
     gDesiredDisplayModeSpecsClassInfo.allowGroupSwitching =
             GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "allowGroupSwitching", "Z");
-    gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMin =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "primaryRefreshRateMin", "F");
-    gDesiredDisplayModeSpecsClassInfo.primaryRefreshRateMax =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "primaryRefreshRateMax", "F");
-    gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMin =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRefreshRateMin", "F");
-    gDesiredDisplayModeSpecsClassInfo.appRequestRefreshRateMax =
-            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRefreshRateMax", "F");
+    gDesiredDisplayModeSpecsClassInfo.primaryRanges =
+            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "primaryRanges",
+                            "Landroid/view/SurfaceControl$RefreshRateRanges;");
+    gDesiredDisplayModeSpecsClassInfo.appRequestRanges =
+            GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRanges",
+                            "Landroid/view/SurfaceControl$RefreshRateRanges;");
 
     jclass jankDataClazz =
                 FindClassOrDie(env, "android/view/SurfaceControl$JankData");
diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp
index 40f6e4f..5c71f69 100644
--- a/core/jni/fd_utils.cpp
+++ b/core/jni/fd_utils.cpp
@@ -580,6 +580,7 @@
       // TODO(narayan): This will be an error in a future android release.
       // error = true;
       // ALOGW("Zygote closed file descriptor %d.", it->first);
+      delete it->second;
       it = open_fd_map_.erase(it);
     } else {
       // The entry from the file descriptor table is still open. Restat
diff --git a/core/proto/android/view/imefocuscontroller.proto b/core/proto/android/view/imefocuscontroller.proto
index ff9dee6..ccde9b7 100644
--- a/core/proto/android/view/imefocuscontroller.proto
+++ b/core/proto/android/view/imefocuscontroller.proto
@@ -25,6 +25,6 @@
  */
 message ImeFocusControllerProto {
     optional bool has_ime_focus = 1;
-    optional string served_view = 2;
-    optional string next_served_view = 3;
+    optional string served_view = 2 [deprecated = true];
+    optional string next_served_view = 3 [deprecated = true];
 }
\ No newline at end of file
diff --git a/core/proto/android/view/inputmethod/inputmethodmanager.proto b/core/proto/android/view/inputmethod/inputmethodmanager.proto
index 9fed0ef..ea5f1e8 100644
--- a/core/proto/android/view/inputmethod/inputmethodmanager.proto
+++ b/core/proto/android/view/inputmethod/inputmethodmanager.proto
@@ -29,4 +29,6 @@
     optional int32 display_id = 3;
     optional bool active = 4;
     optional bool served_connecting = 5;
+    optional string served_view = 6;
+    optional string next_served_view = 7;
 }
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 1f23eb6..554b153 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -668,7 +668,6 @@
     <protected-broadcast android:name="android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" />
     <protected-broadcast android:name="android.media.tv.action.WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED" />
     <protected-broadcast android:name="android.media.tv.action.CHANNEL_BROWSABLE_REQUESTED" />
-    <protected-broadcast android:name="com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER" />
 
     <!-- Time zone rules update intents fired by the system server -->
     <protected-broadcast android:name="com.android.intent.action.timezone.RULES_UPDATE_OPERATION" />
@@ -1152,7 +1151,28 @@
                 android:protectionLevel="dangerous" />
 
     <!-- Allows an application to write to external storage.
-         <p class="note"><strong>Note:</strong> If <em>both</em> your <a
+         <p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or
+         higher, this permission has no effect.
+
+         <p>If your app is on a device that runs API level 19 or higher, you don't need to declare
+         this permission to read and write files in your application-specific directories returned
+         by {@link android.content.Context#getExternalFilesDir} and
+         {@link android.content.Context#getExternalCacheDir}.
+
+         <p>Learn more about how to
+         <a href="{@docRoot}training/data-storage/shared/media#update-other-apps-files">modify media
+         files</a> that your app doesn't own, and how to
+         <a href="{@docRoot}training/data-storage/shared/documents-files">modify non-media files</a>
+         that your app doesn't own.
+
+         <p>If your app is a file manager and needs broad access to external storage files, then
+         the system must place your app on an allowlist so that you can successfully request the
+         <a href="#MANAGE_EXTERNAL_STORAGE><code>MANAGE_EXTERNAL_STORAGE</code></a> permission.
+         Learn more about the appropriate use cases for
+         <a href="{@docRoot}training/data-storage/manage-all-files>managing all files on a storage
+         device</a>.
+
+         <p>If <em>both</em> your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code
          minSdkVersion}</a> and <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
@@ -1160,12 +1180,6 @@
          grants your app this permission. If you don't need this permission, be sure your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code
          targetSdkVersion}</a> is 4 or higher.
-         <p>Starting in API level 19, this permission is <em>not</em> required to
-         read/write files in your application-specific directories returned by
-         {@link android.content.Context#getExternalFilesDir} and
-         {@link android.content.Context#getExternalCacheDir}.
-         <p>If this permission is not allowlisted for an app that targets an API level before
-         {@link android.os.Build.VERSION_CODES#Q} this permission cannot be granted to apps.</p>
          <p>Protection level: dangerous</p>
     -->
     <permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -3043,6 +3057,12 @@
     <permission android:name="android.permission.CREATE_USERS"
         android:protectionLevel="signature" />
 
+    <!-- @SystemApi @hide Allows an application to set user association
+         with a certain subscription. Used by Enterprise to associate a
+         subscription with a work or personal profile. -->
+    <permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi @hide Allows an application to call APIs that allow it to query users on the
          device. -->
     <permission android:name="android.permission.QUERY_USERS"
@@ -3156,6 +3176,13 @@
 
     <!-- Allows an application to call
         {@link android.app.ActivityManager#killBackgroundProcesses}.
+        <p>As of Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+        the {@link android.app.ActivityManager#killBackgroundProcesses} is no longer available to
+        third party applications. For backwards compatibility, the background processes of the
+        caller's own package will still be killed when calling this API. If the caller has
+        the system permission {@code KILL_ALL_BACKGROUND_PROCESSES}, other processes will be
+        killed too.
+
          <p>Protection level: normal
     -->
     <permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"
@@ -3163,6 +3190,16 @@
         android:description="@string/permdesc_killBackgroundProcesses"
         android:protectionLevel="normal" />
 
+    <!-- @SystemApi @hide Allows an application to call
+        {@link android.app.ActivityManager#killBackgroundProcesses}
+        to kill background processes of other apps.
+         <p>Not for use by third-party applications.
+    -->
+    <permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"
+        android:label="@string/permlab_killBackgroundProcesses"
+        android:description="@string/permdesc_killBackgroundProcesses"
+        android:protectionLevel="signature|privileged" />
+
     <!-- @SystemApi @hide Allows an application to query process states and current
          OOM adjustment scores.
          <p>Not for use by third-party applications. -->
@@ -4261,6 +4298,13 @@
     <permission android:name="android.permission.BIND_AUTOFILL_SERVICE"
         android:protectionLevel="signature" />
 
+    <!-- Must be required by a CredentialProviderService to ensure that only the
+         system can bind to it.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
+                android:protectionLevel="signature" />
+
    <!-- Alternative version of android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE.
         This permission was renamed during the O previews but it was supported on the final O
         release, so we need to carry it over.
@@ -6571,6 +6615,13 @@
                 android:protectionLevel="signature" />
     <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" />
 
+    <!-- Allows financed device kiosk apps to perform actions on the Device Lock service
+         <p>Protection level: internal|role
+         <p>Intended for use by the FINANCED_DEVICE_KIOSK role only.
+    -->
+    <permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE"
+                android:protectionLevel="internal|role" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/res/res/anim/dream_activity_close_exit.xml b/core/res/res/anim/dream_activity_close_exit.xml
index c4599da..8df624f 100644
--- a/core/res/res/anim/dream_activity_close_exit.xml
+++ b/core/res/res/anim/dream_activity_close_exit.xml
@@ -19,5 +19,5 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="1.0"
     android:toAlpha="0.0"
-    android:duration="100" />
+    android:duration="@integer/config_dreamCloseAnimationDuration" />
 
diff --git a/core/res/res/anim/dream_activity_open_enter.xml b/core/res/res/anim/dream_activity_open_enter.xml
index 9e1c6e2..d6d9c5c 100644
--- a/core/res/res/anim/dream_activity_open_enter.xml
+++ b/core/res/res/anim/dream_activity_open_enter.xml
@@ -22,5 +22,5 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="0.0"
     android:toAlpha="1.0"
-    android:duration="1000" />
+    android:duration="@integer/config_dreamOpenAnimationDuration" />
 
diff --git a/core/res/res/anim/dream_activity_open_exit.xml b/core/res/res/anim/dream_activity_open_exit.xml
index 740f528..2c2e501 100644
--- a/core/res/res/anim/dream_activity_open_exit.xml
+++ b/core/res/res/anim/dream_activity_open_exit.xml
@@ -22,4 +22,4 @@
 <alpha xmlns:android="http://schemas.android.com/apk/res/android"
     android:fromAlpha="1.0"
     android:toAlpha="1.0"
-    android:duration="1000" />
+    android:duration="@integer/config_dreamOpenAnimationDuration" />
diff --git a/core/res/res/drawable-hdpi/ic_notification_ime_default.png b/core/res/res/drawable-hdpi/ic_notification_ime_default.png
deleted file mode 100644
index 369c88d..0000000
--- a/core/res/res/drawable-hdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-mdpi/ic_notification_ime_default.png b/core/res/res/drawable-mdpi/ic_notification_ime_default.png
deleted file mode 100644
index 7d97eb5..0000000
--- a/core/res/res/drawable-mdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xhdpi/ic_notification_ime_default.png
deleted file mode 100644
index 900801a..0000000
--- a/core/res/res/drawable-xhdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png b/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png
deleted file mode 100644
index 6c8222e..0000000
--- a/core/res/res/drawable-xxhdpi/ic_notification_ime_default.png
+++ /dev/null
Binary files differ
diff --git a/core/res/res/values-af/strings.xml b/core/res/res/values-af/strings.xml
index 4b9abad..0ebce40 100644
--- a/core/res/res/values-af/strings.xml
+++ b/core/res/res/values-af/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Snelsluit"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nuwe kennisgewing"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuele sleutelbord"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fisieke sleutelbord"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sekuriteit"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Motormodus"</string>
diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml
index b6fdcdf..a861f3c 100644
--- a/core/res/res/values-am/strings.xml
+++ b/core/res/res/values-am/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"መቆለፊያ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"አዲስ ማሳወቂያ"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ምናባዊ የቁልፍ ሰሌዳ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"አካላዊ ቁልፍ ሰሌዳ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ደህንነት"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"የመኪና ሁነታ"</string>
diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml
index 977cf8e..7623fb1 100644
--- a/core/res/res/values-ar/strings.xml
+++ b/core/res/res/values-ar/strings.xml
@@ -268,7 +268,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"إلغاء التأمين"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"إشعار جديد"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"لوحة المفاتيح الافتراضية"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"لوحة المفاتيح الخارجية"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"الأمان"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"وضع السيارة"</string>
diff --git a/core/res/res/values-as/strings.xml b/core/res/res/values-as/strings.xml
index dbce595..3a97baf 100644
--- a/core/res/res/values-as/strings.xml
+++ b/core/res/res/values-as/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"লকডাউন"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"৯৯৯+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"নতুন জাননী"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ভাৰ্শ্বুৱল কীব\'ৰ্ড"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"কায়িক কীব’ৰ্ড"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"সুৰক্ষা"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"গাড়ী ম\'ড"</string>
diff --git a/core/res/res/values-az/strings.xml b/core/res/res/values-az/strings.xml
index 586adef..0b361ac 100644
--- a/core/res/res/values-az/strings.xml
+++ b/core/res/res/values-az/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Kilidləyin"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Yeni bildiriş"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual klaviatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fiziki klaviatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Güvənlik"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Avtomobil rejimi"</string>
diff --git a/core/res/res/values-b+sr+Latn/strings.xml b/core/res/res/values-b+sr+Latn/strings.xml
index c19338d..078c098 100644
--- a/core/res/res/values-b+sr+Latn/strings.xml
+++ b/core/res/res/values-b+sr+Latn/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zaključavanje"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Novo obaveštenje"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelna tastatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizička tastatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Bezbednost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Režim rada u automobilu"</string>
diff --git a/core/res/res/values-be/strings.xml b/core/res/res/values-be/strings.xml
index ab338c7..023e82c 100644
--- a/core/res/res/values-be/strings.xml
+++ b/core/res/res/values-be/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Блакіроўка"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Новае апавяшчэнне"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Віртуальная клавіятура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Фізічная клавіятура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Бяспека"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Рэжым \"У машыне\""</string>
diff --git a/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml
index 46ab1ad..aaa080a 100644
--- a/core/res/res/values-bg/strings.xml
+++ b/core/res/res/values-bg/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Заключване"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ново известие"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуална клавиатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физическа клавиатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Сигурност"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Моторежим"</string>
diff --git a/core/res/res/values-bn/strings.xml b/core/res/res/values-bn/strings.xml
index ed77eef..ee1db8e 100644
--- a/core/res/res/values-bn/strings.xml
+++ b/core/res/res/values-bn/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"লকডাউন"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"৯৯৯+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"নতুন বিজ্ঞপ্তি"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ভার্চুয়াল কীবোর্ড"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ফিজিক্যাল কীবোর্ড"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"নিরাপত্তা"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"গাড়ি মোড"</string>
diff --git a/core/res/res/values-bs/strings.xml b/core/res/res/values-bs/strings.xml
index 081d8f2..20f6bc1 100644
--- a/core/res/res/values-bs/strings.xml
+++ b/core/res/res/values-bs/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zaključavanje"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Novo obavještenje"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelna tastatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizička tastatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sigurnost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Način rada u automobilu"</string>
diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml
index aad5668..2142b60 100644
--- a/core/res/res/values-ca/strings.xml
+++ b/core/res/res/values-ca/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueig de seguretat"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"+999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificació nova"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclat virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclat físic"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguretat"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode de cotxe"</string>
diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml
index e9d5ab2..7720d08 100644
--- a/core/res/res/values-cs/strings.xml
+++ b/core/res/res/values-cs/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zamknuto"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nové oznámení"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuální klávesnice"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fyzická klávesnice"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Zabezpečení"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Režim Auto"</string>
diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml
index edb962c..ecd6407 100644
--- a/core/res/res/values-da/strings.xml
+++ b/core/res/res/values-da/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lås enhed"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ny notifikation"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelt tastatur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysisk tastatur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sikkerhed"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Biltilstand"</string>
diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml
index b7a0b02..3d5985c9 100644
--- a/core/res/res/values-de/strings.xml
+++ b/core/res/res/values-de/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Sperren"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Neue Benachrichtigung"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Bildschirmtastatur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physische Tastatur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sicherheit"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automodus"</string>
diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml
index a9dd1cf..f84a9fb 100644
--- a/core/res/res/values-el/strings.xml
+++ b/core/res/res/values-el/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Κλείδωμα"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Νέα ειδοποίηση"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Εικονικό πληκτρολόγιο"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Κανονικό πληκτρολόγιο"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Ασφάλεια"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Λειτουργία αυτοκινήτου"</string>
diff --git a/core/res/res/values-en-rAU/strings.xml b/core/res/res/values-en-rAU/strings.xml
index 1cc8d50..4c0510b 100644
--- a/core/res/res/values-en-rAU/strings.xml
+++ b/core/res/res/values-en-rAU/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
diff --git a/core/res/res/values-en-rCA/strings.xml b/core/res/res/values-en-rCA/strings.xml
index 6fa02f3..875ddf9 100644
--- a/core/res/res/values-en-rCA/strings.xml
+++ b/core/res/res/values-en-rCA/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
diff --git a/core/res/res/values-en-rGB/strings.xml b/core/res/res/values-en-rGB/strings.xml
index fac706e..6e034b7 100644
--- a/core/res/res/values-en-rGB/strings.xml
+++ b/core/res/res/values-en-rGB/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
diff --git a/core/res/res/values-en-rIN/strings.xml b/core/res/res/values-en-rIN/strings.xml
index 55c121ac..643f27f 100644
--- a/core/res/res/values-en-rIN/strings.xml
+++ b/core/res/res/values-en-rIN/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"New notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Physical keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Security"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
diff --git a/core/res/res/values-en-rXC/strings.xml b/core/res/res/values-en-rXC/strings.xml
index 1b190e3..91e99ff 100644
--- a/core/res/res/values-en-rXC/strings.xml
+++ b/core/res/res/values-en-rXC/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‎‎‎‎‏‏‎‎‏‎‎‎‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‎‎‏‏‎Lockdown‎‏‎‎‏‎"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‏‎‎‎‏‏‎‏‏‏‎‎‎‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‏‏‎‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‏‏‏‎‎‏‎‎‏‏‎999+‎‏‎‎‏‎"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‏‏‎‎‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‏‏‎‎‏‎‎‏‎‏‏‏‎‎‏‏‎‏‏‎‎‏‎‏‎‏‏‏‎New notification‎‏‎‎‏‎"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‎‏‏‏‏‎‎‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‎‏‎‎‏‎‎‎‎‎‏‎‏‏‏‎‏‏‏‎Virtual keyboard‎‏‎‎‏‎"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‎‏‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‎‏‎‎‎‎‏‏‎‎‏‏‎‏‎‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‎‎‎‎‎Physical keyboard‎‏‎‎‏‎"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‎‎‏‏‎‎‏‎‏‏‏‎‎‏‏‏‎‏‏‎‎‎‏‎‏‎‏‎‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‏‏‎‏‎‎‏‎Security‎‏‎‎‏‎"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‎‏‏‎‏‎‏‎‎‏‏‎‏‎‏‎‏‏‎‏‏‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‎‎‏‎‏‎‎‎Car mode‎‏‎‎‏‎"</string>
diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml
index 106935c2..6a45205 100644
--- a/core/res/res/values-es-rUS/strings.xml
+++ b/core/res/res/values-es-rUS/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueo"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificación nueva"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguridad"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo auto"</string>
@@ -1970,8 +1969,8 @@
     <string name="usb_mtp_launch_notification_description" msgid="6942535713629852684">"Presiona para ver archivos"</string>
     <string name="pin_target" msgid="8036028973110156895">"Fijar"</string>
     <string name="pin_specific_target" msgid="7824671240625957415">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
-    <string name="unpin_target" msgid="3963318576590204447">"No fijar"</string>
-    <string name="unpin_specific_target" msgid="3859828252160908146">"No fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+    <string name="unpin_target" msgid="3963318576590204447">"Dejar de fijar"</string>
+    <string name="unpin_specific_target" msgid="3859828252160908146">"Dejar de fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
     <string name="app_info" msgid="6113278084877079851">"Información de apps"</string>
     <string name="negative_duration" msgid="1938335096972945232">"−<xliff:g id="TIME">%1$s</xliff:g>"</string>
     <string name="demo_starting_message" msgid="6577581216125805905">"Iniciando demostración…"</string>
diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml
index 3ae013b..66f67b3 100644
--- a/core/res/res/values-es/strings.xml
+++ b/core/res/res/values-es/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueo de seguridad"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt; 999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificación nueva"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguridad"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo de coche"</string>
diff --git a/core/res/res/values-et/strings.xml b/core/res/res/values-et/strings.xml
index 182aa65..349a6b2 100644
--- a/core/res/res/values-et/strings.xml
+++ b/core/res/res/values-et/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lukustamine"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Uus märguanne"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuaalne klaviatuur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Füüsiline klaviatuur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Turvalisus"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Autorežiim"</string>
diff --git a/core/res/res/values-eu/strings.xml b/core/res/res/values-eu/strings.xml
index 31cc0b6..d4759d5 100644
--- a/core/res/res/values-eu/strings.xml
+++ b/core/res/res/values-eu/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blokeoa"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Jakinarazpen berria"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teklatu birtuala"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teklatu fisikoa"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurtasuna"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Auto modua"</string>
diff --git a/core/res/res/values-fa/strings.xml b/core/res/res/values-fa/strings.xml
index 58a7f62..4064353 100644
--- a/core/res/res/values-fa/strings.xml
+++ b/core/res/res/values-fa/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"قفل همه"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"۹۹۹+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"اعلان جدید"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"صفحه‌‌کلید مجازی"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"صفحه‌کلید فیزیکی"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"امنیت"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"حالت خودرو"</string>
diff --git a/core/res/res/values-fi/strings.xml b/core/res/res/values-fi/strings.xml
index 31d2571..8fedfb7 100644
--- a/core/res/res/values-fi/strings.xml
+++ b/core/res/res/values-fi/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lukitse"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Uusi ilmoitus"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuaalinen näppäimistö"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fyysinen näppäimistö"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Turvallisuus"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Autotila"</string>
diff --git a/core/res/res/values-fr-rCA/strings.xml b/core/res/res/values-fr-rCA/strings.xml
index 5150da9..e63b734 100644
--- a/core/res/res/values-fr-rCA/strings.xml
+++ b/core/res/res/values-fr-rCA/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Verrouillage"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nouvelle notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Clavier virtuel"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Clavier physique"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sécurité"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode Voiture"</string>
diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml
index 3736890..019fdf2 100644
--- a/core/res/res/values-fr/strings.xml
+++ b/core/res/res/values-fr/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Verrouiller"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nouvelle notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Clavier virtuel"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Clavier physique"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sécurité"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode Voiture"</string>
diff --git a/core/res/res/values-gl/strings.xml b/core/res/res/values-gl/strings.xml
index 7575d68..219299f 100644
--- a/core/res/res/values-gl/strings.xml
+++ b/core/res/res/values-gl/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloquear"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificación nova"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguranza"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo coche"</string>
diff --git a/core/res/res/values-gu/strings.xml b/core/res/res/values-gu/strings.xml
index 42bad0a..90dda1a 100644
--- a/core/res/res/values-gu/strings.xml
+++ b/core/res/res/values-gu/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"લૉકડાઉન"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"નવું નોટિફિકેશન"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"વર્ચ્યુઅલ કીબોર્ડ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ભૌતિક કીબોર્ડ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"સુરક્ષા"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"કાર મોડ"</string>
diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml
index 83cacea..af5bc1f 100644
--- a/core/res/res/values-hi/strings.xml
+++ b/core/res/res/values-hi/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"फ़ाेन लॉक करें"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"नई सूचना"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"वर्चुअल कीबोर्ड"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"सामान्य कीबोर्ड"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"सुरक्षा"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"कार मोड"</string>
@@ -1136,7 +1135,7 @@
     <string name="copy" msgid="5472512047143665218">"कॉपी करें"</string>
     <string name="failed_to_copy_to_clipboard" msgid="725919885138539875">"क्लिपबोर्ड पर कॉपी नहीं हो सका"</string>
     <string name="paste" msgid="461843306215520225">"चिपकाएं"</string>
-    <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे पाठ के रूप में चिपकाएं"</string>
+    <string name="paste_as_plain_text" msgid="7664800665823182587">"सादे टेक्स्ट के रूप में चिपकाएं"</string>
     <string name="replace" msgid="7842675434546657444">"बदलें•"</string>
     <string name="delete" msgid="1514113991712129054">"मिटाएं"</string>
     <string name="copyUrl" msgid="6229645005987260230">"यूआरएल को कॉपी करें"</string>
diff --git a/core/res/res/values-hr/strings.xml b/core/res/res/values-hr/strings.xml
index 6f81009..87df29a 100644
--- a/core/res/res/values-hr/strings.xml
+++ b/core/res/res/values-hr/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zaključavanje"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova obavijest"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtualna tipkovnica"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizička tipkovnica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sigurnost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Način rada u automobilu"</string>
diff --git a/core/res/res/values-hu/strings.xml b/core/res/res/values-hu/strings.xml
index 71687d4..3762fde 100644
--- a/core/res/res/values-hu/strings.xml
+++ b/core/res/res/values-hu/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zárolás"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Új értesítés"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuális billentyűzet"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizikai billentyűzet"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Biztonság"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Autós üzemmód"</string>
diff --git a/core/res/res/values-hy/strings.xml b/core/res/res/values-hy/strings.xml
index b7146f0..a11e24b 100644
--- a/core/res/res/values-hy/strings.xml
+++ b/core/res/res/values-hy/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Արգելափակում"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Նոր ծանուցում"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Վիրտուալ ստեղնաշար"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Ֆիզիկական ստեղնաշար"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Անվտանգություն"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Մեքենայի ռեժիմ"</string>
diff --git a/core/res/res/values-in/strings.xml b/core/res/res/values-in/strings.xml
index 66fc3fb..dbccee9 100644
--- a/core/res/res/values-in/strings.xml
+++ b/core/res/res/values-in/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Kunci total"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notifikasi baru"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Keyboard virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Keyboard fisik"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Keamanan"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mode mobil"</string>
diff --git a/core/res/res/values-is/strings.xml b/core/res/res/values-is/strings.xml
index 64b2340..cfefc03 100644
--- a/core/res/res/values-is/strings.xml
+++ b/core/res/res/values-is/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Læsing"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ný tilkynning"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Sýndarlyklaborð"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Vélbúnaðarlyklaborð"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Öryggi"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Bílastilling"</string>
diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml
index d25bb06..b05bf79 100644
--- a/core/res/res/values-it/strings.xml
+++ b/core/res/res/values-it/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blocco"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nuova notifica"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Tastiera virtuale"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tastiera fisica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sicurezza"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modalità automobile"</string>
diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml
index a1373c3..8656fce 100644
--- a/core/res/res/values-iw/strings.xml
+++ b/core/res/res/values-iw/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"נעילה"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"התראה חדשה"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"מקלדת וירטואלית"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"מקלדת פיזית"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"אבטחה"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"מצב נהיגה"</string>
diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml
index ed55a7f..69d0b9d 100644
--- a/core/res/res/values-ja/strings.xml
+++ b/core/res/res/values-ja/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ロックダウン"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新しい通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"仮想キーボード"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"物理キーボード"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"セキュリティ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"運転モード"</string>
diff --git a/core/res/res/values-ka/strings.xml b/core/res/res/values-ka/strings.xml
index 0b06d7c..6d32f25 100644
--- a/core/res/res/values-ka/strings.xml
+++ b/core/res/res/values-ka/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"დაბლოკვა"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ახალი შეტყობინება"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ვირტუალური კლავიატურა"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ფიზიკური კლავიატურა"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"უსაფრთხოება"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"მანქანის რეჟიმი"</string>
diff --git a/core/res/res/values-kk/strings.xml b/core/res/res/values-kk/strings.xml
index a44d09d..0d9fd3f 100644
--- a/core/res/res/values-kk/strings.xml
+++ b/core/res/res/values-kk/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Құлыптау"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Жаңа хабарландыру"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуалдық пернетақта"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физикалық пернетақта"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Қауіпсіздік"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Көлік режимі"</string>
diff --git a/core/res/res/values-km/strings.xml b/core/res/res/values-km/strings.xml
index 2fada73..0c82b66 100644
--- a/core/res/res/values-km/strings.xml
+++ b/core/res/res/values-km/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ការចាក់​សោ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ការជូនដំណឹងថ្មី"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ក្ដារ​ចុច​និម្មិត"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ក្ដារចុច​រូបវន្ត"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"សុវត្ថិភាព"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"មុខងារ​រថយន្ត"</string>
diff --git a/core/res/res/values-kn/strings.xml b/core/res/res/values-kn/strings.xml
index c39d9f7..e27527f 100644
--- a/core/res/res/values-kn/strings.xml
+++ b/core/res/res/values-kn/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ಲಾಕ್‌ಡೌನ್‌"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ಹೊಸ ಅಧಿಸೂಚನೆ"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ವರ್ಚುಯಲ್ ಕೀಬೋರ್ಡ್"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ಭೌತಿಕ ಕೀಬೋರ್ಡ್‌"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ಭದ್ರತೆ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ಕಾರ್ ಮೋಡ್"</string>
diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml
index 55bae4d..c953a39 100644
--- a/core/res/res/values-ko/strings.xml
+++ b/core/res/res/values-ko/strings.xml
@@ -247,7 +247,7 @@
     <string name="bugreport_message" msgid="5212529146119624326">"현재 기기 상태에 대한 정보를 수집하여 이메일 메시지로 전송합니다. 버그 신고를 시작하여 전송할 준비가 되려면 약간 시간이 걸립니다."</string>
     <string name="bugreport_option_interactive_title" msgid="7968287837902871289">"대화형 보고서"</string>
     <string name="bugreport_option_interactive_summary" msgid="8493795476325339542">"대부분의 경우 이 옵션을 사용합니다. 신고 진행 상황을 추적하고 문제에 대한 세부정보를 입력하고 스크린샷을 찍을 수 있습니다. 신고하기에 시간이 너무 오래 걸리고 사용 빈도가 낮은 일부 섹션을 생략할 수 있습니다."</string>
-    <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 보고서"</string>
+    <string name="bugreport_option_full_title" msgid="7681035745950045690">"전체 신고"</string>
     <string name="bugreport_option_full_summary" msgid="1975130009258435885">"기기가 응답하지 않거나 너무 느리거나 모든 보고서 섹션이 필요한 경우 이 옵션을 사용하여 시스템 방해를 최소화합니다. 세부정보를 추가하거나 스크린샷을 추가로 찍을 수 없습니다."</string>
     <string name="bugreport_countdown" msgid="6418620521782120755">"{count,plural, =1{버그 신고 스크린샷을 #초 후에 찍습니다.}other{버그 신고 스크린샷을 #초 후에 찍습니다.}}"</string>
     <string name="bugreport_screenshot_success_toast" msgid="7986095104151473745">"버그 신고용 스크린샷 촬영 완료"</string>
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"잠금"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"새 알림"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"가상 키보드"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"물리적 키보드"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"보안"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"운전 모드"</string>
diff --git a/core/res/res/values-ky/strings.xml b/core/res/res/values-ky/strings.xml
index ad01dafc..dccc4a6 100644
--- a/core/res/res/values-ky/strings.xml
+++ b/core/res/res/values-ky/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Бекем кулпулоо"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Жаңы эскертме"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуалдык баскычтоп"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Аппараттык баскычтоп"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Коопсуздук"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Унаа режими"</string>
diff --git a/core/res/res/values-lo/strings.xml b/core/res/res/values-lo/strings.xml
index 02df227..c6524de 100644
--- a/core/res/res/values-lo/strings.xml
+++ b/core/res/res/values-lo/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ລັອກໄວ້"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ການແຈ້ງເຕືອນໃໝ່"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ແປ້ນພິມສະເໝືອນ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ແປ້ນພິມພາຍນອກ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ຄວາມປອດໄພ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ໂໝດຂັບລົດ"</string>
diff --git a/core/res/res/values-lt/strings.xml b/core/res/res/values-lt/strings.xml
index 4543ef6..adf30e8 100644
--- a/core/res/res/values-lt/strings.xml
+++ b/core/res/res/values-lt/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Užrakinimas"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Naujas pranešimas"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtualioji klaviatūra"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizinė klaviatūra"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sauga"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automobilio režimas"</string>
diff --git a/core/res/res/values-lv/strings.xml b/core/res/res/values-lv/strings.xml
index 64c855b..5631521 100644
--- a/core/res/res/values-lv/strings.xml
+++ b/core/res/res/values-lv/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloķēšana"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"Pārsniedz"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Jauns paziņojums"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuālā tastatūra"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fiziskā tastatūra"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Drošība"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automašīnas režīms"</string>
diff --git a/core/res/res/values-mk/strings.xml b/core/res/res/values-mk/strings.xml
index 0af4cdd..a45d0a7 100644
--- a/core/res/res/values-mk/strings.xml
+++ b/core/res/res/values-mk/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Заклучување"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ново известување"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуелна тастатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физичка тастатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безбедност"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим на работа во автомобил"</string>
diff --git a/core/res/res/values-ml/strings.xml b/core/res/res/values-ml/strings.xml
index bc12c07..492cd54 100644
--- a/core/res/res/values-ml/strings.xml
+++ b/core/res/res/values-ml/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ലോക്ക്‌ഡൗൺ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"പുതിയ അറിയിപ്പ്"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"വെർച്വൽ കീബോഡ്"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ഫിസിക്കൽ കീബോഡ്"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"സുരക്ഷ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"കാർ മോഡ്"</string>
diff --git a/core/res/res/values-mn/strings.xml b/core/res/res/values-mn/strings.xml
index 4e8c314..2c8aaae 100644
--- a/core/res/res/values-mn/strings.xml
+++ b/core/res/res/values-mn/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Түгжих"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Шинэ мэдэгдэл"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуал гар"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Биет гар"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Аюулгүй байдал"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Машины горим"</string>
diff --git a/core/res/res/values-mr/strings.xml b/core/res/res/values-mr/strings.xml
index aa8c1e9..d47fea35 100644
--- a/core/res/res/values-mr/strings.xml
+++ b/core/res/res/values-mr/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"लॉकडाउन"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"नवीन सूचना"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"व्हर्च्युअल कीबोर्ड"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"वास्तविक कीबोर्ड"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"सुरक्षा"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"कार मोड"</string>
diff --git a/core/res/res/values-ms/strings.xml b/core/res/res/values-ms/strings.xml
index 9a6ee3b..deb343d 100644
--- a/core/res/res/values-ms/strings.xml
+++ b/core/res/res/values-ms/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Kunci semua"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Pemberitahuan baharu"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Papan kekunci maya"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Papan kekunci fizikal"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Keselamatan"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mod kereta"</string>
diff --git a/core/res/res/values-my/strings.xml b/core/res/res/values-my/strings.xml
index 716f5d7..5f41672 100644
--- a/core/res/res/values-my/strings.xml
+++ b/core/res/res/values-my/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"လော့ခ်ဒေါင်း"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"၉၉၉+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"အကြောင်းကြားချက်အသစ်"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ပကတိအသွင်ကီးဘုတ်"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"စက်၏ ကီးဘုတ်"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"လုံခြုံရေး"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ကားမုဒ်"</string>
diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml
index 3d16ea7..0c9a983 100644
--- a/core/res/res/values-nb/strings.xml
+++ b/core/res/res/values-nb/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Låsing"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nytt varsel"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuelt tastatur"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysisk tastatur"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Sikkerhet"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Bilmodus"</string>
diff --git a/core/res/res/values-ne/strings.xml b/core/res/res/values-ne/strings.xml
index bdb31bc..e95d48b 100644
--- a/core/res/res/values-ne/strings.xml
+++ b/core/res/res/values-ne/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"लकडाउन गर्नु…"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"९९९+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"नयाँ सूचना"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"भर्चुअल किबोर्ड"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"फिजिकल किबोर्ड"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"सुरक्षा"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"कार मोड"</string>
diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml
index 162d3e8..5723510 100644
--- a/core/res/res/values-nl/strings.xml
+++ b/core/res/res/values-nl/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999 +"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nieuwe melding"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtueel toetsenbord"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysiek toetsenbord"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Beveiliging"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Automodus"</string>
diff --git a/core/res/res/values-or/strings.xml b/core/res/res/values-or/strings.xml
index 440245e..c4150dc 100644
--- a/core/res/res/values-or/strings.xml
+++ b/core/res/res/values-or/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ଲକ୍ କରନ୍ତୁ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ନୂଆ ବିଜ୍ଞପ୍ତି"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ଭର୍ଚୁଆଲ୍‌ କୀ\'ବୋର୍ଡ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ଫିଜିକଲ୍ କୀ’ବୋର୍ଡ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ସୁରକ୍ଷା"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"କାର୍ ମୋଡ୍"</string>
diff --git a/core/res/res/values-pa/strings.xml b/core/res/res/values-pa/strings.xml
index a9e3531..96917c0 100644
--- a/core/res/res/values-pa/strings.xml
+++ b/core/res/res/values-pa/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ਲਾਕਡਾਊਨ"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"ਨਵੀਂ ਸੂਚਨਾ"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ਆਭਾਸੀ ਕੀ-ਬੋਰਡ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"ਭੌਤਿਕ ਕੀ-ਬੋਰਡ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ਸੁਰੱਖਿਆ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"ਕਾਰ ਮੋਡ"</string>
diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml
index 7b47a5c..be9d322 100644
--- a/core/res/res/values-pl/strings.xml
+++ b/core/res/res/values-pl/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blokada"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nowe powiadomienie"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Klawiatura wirtualna"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Klawiatura fizyczna"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Bezpieczeństwo"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Tryb samochodowy"</string>
diff --git a/core/res/res/values-pt-rBR/strings.xml b/core/res/res/values-pt-rBR/strings.xml
index 8635d76..ff352b1 100644
--- a/core/res/res/values-pt-rBR/strings.xml
+++ b/core/res/res/values-pt-rBR/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueio total"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova notificação"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurança"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo carro"</string>
diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml
index 5dc59e1..d343af6 100644
--- a/core/res/res/values-pt-rPT/strings.xml
+++ b/core/res/res/values-pt-rPT/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloquear"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova notificação"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurança"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo automóvel"</string>
diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml
index 8635d76..ff352b1 100644
--- a/core/res/res/values-pt/strings.xml
+++ b/core/res/res/values-pt/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloqueio total"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nova notificação"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Teclado virtual"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Teclado físico"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Segurança"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modo carro"</string>
diff --git a/core/res/res/values-ro/strings.xml b/core/res/res/values-ro/strings.xml
index acd1df6..b560b07 100644
--- a/core/res/res/values-ro/strings.xml
+++ b/core/res/res/values-ro/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blocare strictă"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"˃999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Notificare nouă"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Tastatură virtuală"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tastatură fizică"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Securitate"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Mod Mașină"</string>
diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml
index fb4775b..fbe67e2 100644
--- a/core/res/res/values-ru/strings.xml
+++ b/core/res/res/values-ru/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Блокировка входа"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"&gt;999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Новое уведомление"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуальная клавиатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физическая клавиатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безопасность"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим \"В авто\""</string>
diff --git a/core/res/res/values-si/strings.xml b/core/res/res/values-si/strings.xml
index 098e622..4cec877 100644
--- a/core/res/res/values-si/strings.xml
+++ b/core/res/res/values-si/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"අගුලු දැමීම"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"නව දැනුම්දීම"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"අතථ්‍ය යතුරු පුවරුව"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"භෞතික යතුරු පුවරුව"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ආරක්ෂාව"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"මෝටර් රථ ආකාරය"</string>
diff --git a/core/res/res/values-sk/strings.xml b/core/res/res/values-sk/strings.xml
index 47e3d3b..b98364a 100644
--- a/core/res/res/values-sk/strings.xml
+++ b/core/res/res/values-sk/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Uzamknúť"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Nové upozornenie"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuálna klávesnica"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fyzická klávesnica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Zabezpečenie"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Režim v aute"</string>
diff --git a/core/res/res/values-sl/strings.xml b/core/res/res/values-sl/strings.xml
index 6e722e3..6972abb 100644
--- a/core/res/res/values-sl/strings.xml
+++ b/core/res/res/values-sl/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Zakleni"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999 +"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Novo obvestilo"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Navidezna tipkovnica"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fizična tipkovnica"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Varnost"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Način za avtomobil"</string>
diff --git a/core/res/res/values-sq/strings.xml b/core/res/res/values-sq/strings.xml
index 0a70e9a..cbac0f0 100644
--- a/core/res/res/values-sq/strings.xml
+++ b/core/res/res/values-sq/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Blloko"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Njoftim i ri"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Tastiera virtuale"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tastiera fizike"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Siguria"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Modaliteti \"në makinë\""</string>
diff --git a/core/res/res/values-sr/strings.xml b/core/res/res/values-sr/strings.xml
index 0e50810..d5549e7 100644
--- a/core/res/res/values-sr/strings.xml
+++ b/core/res/res/values-sr/strings.xml
@@ -265,7 +265,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Закључавање"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ново обавештење"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Виртуелна тастатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Физичка тастатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безбедност"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим рада у аутомобилу"</string>
diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml
index f8b3fdf..d1c579d 100644
--- a/core/res/res/values-sv/strings.xml
+++ b/core/res/res/values-sv/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Låsning"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Ny avisering"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtuellt tangentbord"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fysiskt tangentbord"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Säkerhet"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Billäge"</string>
diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml
index 5829fae..2bc3f57 100644
--- a/core/res/res/values-sw/strings.xml
+++ b/core/res/res/values-sw/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Funga"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Arifa mpya"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Kibodi pepe"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Kibodi halisi"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Usalama"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Hali ya gari"</string>
diff --git a/core/res/res/values-ta/strings.xml b/core/res/res/values-ta/strings.xml
index 1533c36..9e48a47 100644
--- a/core/res/res/values-ta/strings.xml
+++ b/core/res/res/values-ta/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"பூட்டு"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"புதிய அறிவிப்பு"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"விர்ச்சுவல் கீபோர்டு"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"கைமுறை கீபோர்டு"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"பாதுகாப்பு"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"கார் பயன்முறை"</string>
diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml
index 313bc80..eff08bc 100644
--- a/core/res/res/values-te/strings.xml
+++ b/core/res/res/values-te/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"లాక్ చేయి"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"కొత్త నోటిఫికేషన్"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"వర్చువల్ కీబోర్డ్"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"భౌతిక కీబోర్డ్"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"సెక్యూరిటీ"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"కార్‌ మోడ్"</string>
diff --git a/core/res/res/values-th/strings.xml b/core/res/res/values-th/strings.xml
index 447b42b..72cbc36 100644
--- a/core/res/res/values-th/strings.xml
+++ b/core/res/res/values-th/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"ปิดล็อก"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"การแจ้งเตือนใหม่"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"แป้นพิมพ์เสมือน"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"แป้นพิมพ์จริง"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"ความปลอดภัย"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"โหมดรถยนต์"</string>
diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml
index 9aaa1b8..e66999d 100644
--- a/core/res/res/values-tl/strings.xml
+++ b/core/res/res/values-tl/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"I-lockdown"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Bagong notification"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual na keyboard"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Pisikal na keyboard"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Seguridad"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Car mode"</string>
diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml
index 2f80037..b6c4b4a 100644
--- a/core/res/res/values-tr/strings.xml
+++ b/core/res/res/values-tr/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Tam gizlilik"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Yeni bildirim"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Sanal klavye"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Fiziksel klavye"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Güvenlik"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Araç modu"</string>
diff --git a/core/res/res/values-uk/strings.xml b/core/res/res/values-uk/strings.xml
index b7bb63b..c408c51 100644
--- a/core/res/res/values-uk/strings.xml
+++ b/core/res/res/values-uk/strings.xml
@@ -266,7 +266,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Блокування"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Нове сповіщення"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Віртуальна клавіатура"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Фізична клавіатура"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Безпека"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Режим автомобіля"</string>
diff --git a/core/res/res/values-ur/strings.xml b/core/res/res/values-ur/strings.xml
index 58e342f..7784431 100644
--- a/core/res/res/values-ur/strings.xml
+++ b/core/res/res/values-ur/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"مقفل"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"‎999+‎"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"نئی اطلاع"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"ورچوئل کی بورڈ"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"فزیکل کی بورڈ"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"سیکیورٹی"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"کار وضع"</string>
diff --git a/core/res/res/values-uz/strings.xml b/core/res/res/values-uz/strings.xml
index cf4478d..5ac689d1 100644
--- a/core/res/res/values-uz/strings.xml
+++ b/core/res/res/values-uz/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Bloklash"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Yangi bildirishnoma"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Virtual klaviatura"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Tashqi klaviatura"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Xavfsizlik"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Avtomobil rejimi"</string>
diff --git a/core/res/res/values-vi/strings.xml b/core/res/res/values-vi/strings.xml
index 1a1bb91..e1b479c 100644
--- a/core/res/res/values-vi/strings.xml
+++ b/core/res/res/values-vi/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Khóa"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Thông báo mới"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Bàn phím ảo"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Bàn phím vật lý"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Bảo mật"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Chế độ trên ô tô"</string>
diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml
index 4cb587b..5fce25e 100644
--- a/core/res/res/values-zh-rCN/strings.xml
+++ b/core/res/res/values-zh-rCN/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"锁定"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"虚拟键盘"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"实体键盘"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"安全性"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"车载模式"</string>
diff --git a/core/res/res/values-zh-rHK/strings.xml b/core/res/res/values-zh-rHK/strings.xml
index b9185b7..61a3f20 100644
--- a/core/res/res/values-zh-rHK/strings.xml
+++ b/core/res/res/values-zh-rHK/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"鎖定"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"虛擬鍵盤"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"實體鍵盤"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"安全性"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"車用模式"</string>
diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml
index 4baced8..10dc699 100644
--- a/core/res/res/values-zh-rTW/strings.xml
+++ b/core/res/res/values-zh-rTW/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"鎖定"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"超過 999"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"新通知"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"虛擬鍵盤"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"實體鍵盤"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"安全性"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"車用模式"</string>
diff --git a/core/res/res/values-zu/strings.xml b/core/res/res/values-zu/strings.xml
index 23a557a..66d639e 100644
--- a/core/res/res/values-zu/strings.xml
+++ b/core/res/res/values-zu/strings.xml
@@ -264,7 +264,6 @@
     <string name="global_action_lockdown" msgid="2475471405907902963">"Khiya"</string>
     <string name="status_bar_notification_info_overflow" msgid="3330152558746563475">"999+"</string>
     <string name="notification_hidden_text" msgid="2835519769868187223">"Isaziso esisha"</string>
-    <string name="notification_channel_virtual_keyboard" msgid="6465975799223304567">"Ikhibhodi ebonakalayo"</string>
     <string name="notification_channel_physical_keyboard" msgid="5417306456125988096">"Ikhibhodi ephathekayo"</string>
     <string name="notification_channel_security" msgid="8516754650348238057">"Ukuphepha"</string>
     <string name="notification_channel_car_mode" msgid="2123919247040988436">"Imodi yemoto"</string>
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index b5c7ea6..eac2b94 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3062,6 +3062,20 @@
         <attr name="canDisplayOnRemoteDevices" format="boolean"/>
         <attr name="allowUntrustedActivityEmbedding" />
         <attr name="knownActivityEmbeddingCerts" />
+        <!-- Specifies the category of the target display the activity is expected to run on. Upon
+             creation, a virtual display can specify which display categories it supports and one of
+             the category must be present in the activity's manifest to allow this activity to run.
+             The default value is {@code null}, which indicates the activity does not belong to a
+             restricted display category and thus can only run on a display that didn't specify any
+             display categories. Each activity can only specify one category it targets to but a
+             virtual display can accommodate multiple restricted categories.
+
+             <p> This field should be formatted as a Java-language-style free form string(for
+             example, com.google.automotive_entertainment), which may contain uppercase or lowercase
+             letters ('A' through 'Z'), numbers, and underscores ('_') but may only start with
+             letters.
+         -->
+        <attr name="targetDisplayCategory" format="string"/>
     </declare-styleable>
 
     <!-- The <code>activity-alias</code> tag declares a new
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index caa67de..173908d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2447,6 +2447,11 @@
     <!-- Whether dreams are disabled when ambient mode is suppressed. -->
     <bool name="config_dreamsDisabledByAmbientModeSuppressionConfig">false</bool>
 
+    <!-- The duration in milliseconds of the dream opening animation.  -->
+    <integer name="config_dreamOpenAnimationDuration">250</integer>
+    <!-- The duration in milliseconds of the dream closing animation.  -->
+    <integer name="config_dreamCloseAnimationDuration">100</integer>
+
     <!-- Whether to dismiss the active dream when an activity is started. Doesn't apply to
          assistant activities (ACTIVITY_TYPE_ASSISTANT) -->
     <bool name="config_dismissDreamOnActivityStart">false</bool>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index a1d73ff..f2a16d3 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -117,4 +117,22 @@
     <!-- Whether using the new SubscriptionManagerService or the old SubscriptionController -->
     <bool name="config_using_subscription_manager_service">false</bool>
     <java-symbol type="bool" name="config_using_subscription_manager_service" />
+
+    <!-- Boolean indicating whether the emergency numbers for a country, sourced from modem/config,
+         should be ignored if that country is 'locked' (i.e. ignore_modem_config set to true) in
+         Android Emergency DB. If this value is true, emergency numbers for a country, sourced from
+         modem/config, will be ignored if that country is 'locked' in Android Emergency DB. -->
+    <bool name="ignore_modem_config_emergency_numbers">false</bool>
+    <java-symbol type="bool" name="ignore_modem_config_emergency_numbers" />
+
+    <!-- Boolean indicating whether emergency numbers routing from the android emergency number
+         database should be ignored (i.e. routing will always be set to UNKNOWN). If this value is
+         true, routing from the android emergency number database will be ignored. -->
+    <bool name="ignore_emergency_number_routing_from_db">false</bool>
+    <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" />
+
+    <!-- Whether "Virtual DSDA", i.e. in-call IMS connectivity can be provided on both subs with
+         only single logical modem, by using its data connection in addition to cellular IMS. -->
+    <bool name="config_enable_virtual_dsda">false</bool>
+    <java-symbol type="bool" name="config_enable_virtual_dsda" />
 </resources>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index d03d206..61229cb 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -116,6 +116,7 @@
     <public name="handwritingBoundsOffsetBottom" />
     <public name="accessibilityDataPrivate" />
     <public name="enableTextStylingShortcuts" />
+    <public name="targetDisplayCategory"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01cd0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 5f99113..509de33 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -202,6 +202,11 @@
     <!-- Displayed to tell the user that they cannot change the caller ID setting. -->
     <string name="CLIRPermanent">You can\'t change the caller ID setting.</string>
 
+    <!-- Notification title to tell the user that auto data switch has occurred. [CHAR LIMIT=NOTIF_TITLE] -->
+    <string name="auto_data_switch_title">Switched data to <xliff:g id="carrierDisplay" example="Verizon">%s</xliff:g></string>
+    <!-- Notification content to tell the user that auto data switch can be disabled at settings. [CHAR LIMIT=NOTIF_BODY] -->
+    <string name="auto_data_switch_content">You can change this anytime in Settings</string>
+
     <!-- Notification title to tell the user that data service is blocked by access control. [CHAR LIMIT=NOTIF_TITLE] -->
     <string name="RestrictedOnDataTitle">No mobile data service</string>
     <!-- Notification title to tell the user that emergency calling is blocked by access control. [CHAR LIMIT=NOTIF_TITLE] -->
@@ -741,9 +746,6 @@
     <!-- Text shown in place of notification contents when the notification is hidden on a secure lockscreen -->
     <string name="notification_hidden_text">New notification</string>
 
-    <!-- Text shown when viewing channel settings for notifications related to the virtual keyboard -->
-    <string name="notification_channel_virtual_keyboard">Virtual keyboard</string>
-
     <!-- Text shown when viewing channel settings for notifications related to the hardware keyboard -->
     <string name="notification_channel_physical_keyboard">Physical keyboard</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b94d799..476d36d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -602,6 +602,8 @@
   <java-symbol type="string" name="RestrictedOnEmergencyTitle" />
   <java-symbol type="string" name="RestrictedOnNormalTitle" />
   <java-symbol type="string" name="RestrictedStateContent" />
+  <java-symbol type="string" name="auto_data_switch_title" />
+  <java-symbol type="string" name="auto_data_switch_content" />
   <java-symbol type="string" name="RestrictedStateContentMsimTemplate" />
   <java-symbol type="string" name="notification_channel_network_alert" />
   <java-symbol type="string" name="notification_channel_call_forward" />
@@ -1992,7 +1994,6 @@
   <java-symbol type="color" name="config_defaultNotificationColor" />
   <java-symbol type="color" name="decor_view_status_guard" />
   <java-symbol type="color" name="decor_view_status_guard_light" />
-  <java-symbol type="drawable" name="ic_notification_ime_default" />
   <java-symbol type="drawable" name="ic_menu_refresh" />
   <java-symbol type="drawable" name="ic_settings" />
   <java-symbol type="drawable" name="ic_voice_search" />
@@ -2237,6 +2238,8 @@
   <java-symbol type="string" name="config_dreamsDefaultComponent" />
   <java-symbol type="bool" name="config_dreamsDisabledByAmbientModeSuppressionConfig" />
   <java-symbol type="bool" name="config_dreamsOnlyEnabledForSystemUser" />
+  <java-symbol type="integer" name="config_dreamOpenAnimationDuration" />
+  <java-symbol type="integer" name="config_dreamCloseAnimationDuration" />
   <java-symbol type="array" name="config_supportedDreamComplications" />
   <java-symbol type="array" name="config_disabledDreamComponents" />
   <java-symbol type="bool" name="config_dismissDreamOnActivityStart" />
@@ -3724,7 +3727,6 @@
   <java-symbol type="integer" name="config_maxUiWidth" />
 
   <!-- system notification channels -->
-  <java-symbol type="string" name="notification_channel_virtual_keyboard" />
   <java-symbol type="string" name="notification_channel_physical_keyboard" />
   <java-symbol type="string" name="notification_channel_security" />
   <java-symbol type="string" name="notification_channel_car_mode" />
diff --git a/core/tests/BroadcastRadioTests/Android.bp b/core/tests/BroadcastRadioTests/Android.bp
index 113f45d..7cb64c8 100644
--- a/core/tests/BroadcastRadioTests/Android.bp
+++ b/core/tests/BroadcastRadioTests/Android.bp
@@ -23,23 +23,32 @@
 
 android_test {
     name: "BroadcastRadioTests",
+    srcs: ["src/**/*.java"],
     privileged: true,
     certificate: "platform",
     // TODO(b/13282254): uncomment when b/13282254 is fixed
     // sdk_version: "current"
     platform_apis: true,
-    static_libs: [
-        "compatibility-device-util-axt",
-        "androidx.test.rules",
-        "testng",
-        "services.core",
-    ],
-    libs: ["android.test.base"],
-    srcs: ["src/**/*.java"],
     dex_preopt: {
         enabled: false,
     },
     optimize: {
         enabled: false,
     },
+    static_libs: [
+        "services.core",
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "testng",
+        "mockito-target-extended",
+    ],
+    libs: ["android.test.base"],
+    test_suites: [
+        "general-tests",
+    ],
+    // mockito-target-inline dependency
+    jni_libs: [
+        "libcarservicejni",
+        "libdexmakerjvmtiagent",
+    ],
 }
diff --git a/core/tests/BroadcastRadioTests/AndroidManifest.xml b/core/tests/BroadcastRadioTests/AndroidManifest.xml
index ce12cc9..869b484 100644
--- a/core/tests/BroadcastRadioTests/AndroidManifest.xml
+++ b/core/tests/BroadcastRadioTests/AndroidManifest.xml
@@ -19,7 +19,7 @@
 
     <uses-permission android:name="android.permission.ACCESS_BROADCAST_RADIO" />
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
index 11eb158..3f35e99 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
@@ -33,11 +33,9 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
-import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
 
 import org.junit.After;
 import org.junit.Before;
@@ -47,6 +45,7 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -56,8 +55,7 @@
 /**
  * A test for broadcast radio API.
  */
-@RunWith(AndroidJUnit4.class)
-@MediumTest
+@RunWith(MockitoJUnitRunner.class)
 public class RadioTunerTest {
     private static final String TAG = "BroadcastRadioTests.RadioTuner";
 
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
new file mode 100644
index 0000000..42143b9
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio.tests.unittests;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.ProgramSelector;
+import android.util.ArrayMap;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public final class RadioAnnouncementTest {
+    private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 90500);
+    private static final ProgramSelector FM_PROGRAM_SELECTOR = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, /* secondaryIds= */ null,
+            /* vendorIds= */ null);
+    private static final int TRAFFIC_ANNOUNCEMENT_TYPE = Announcement.TYPE_TRAFFIC;
+    private static final Map<String, String> VENDOR_INFO = createVendorInfo();
+    private static final Announcement TEST_ANNOUNCEMENT =
+            new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO);
+
+    @Test
+    public void constructor_withNullSelector_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            new Announcement(/* selector= */ null, TRAFFIC_ANNOUNCEMENT_TYPE, VENDOR_INFO);
+        });
+
+        assertWithMessage("Exception for null program selector in announcement constructor")
+                .that(thrown).hasMessageThat().contains("Program selector cannot be null");
+    }
+
+    @Test
+    public void constructor_withNullVendorInfo_fails() {
+        NullPointerException thrown = assertThrows(NullPointerException.class, () -> {
+            new Announcement(FM_PROGRAM_SELECTOR, TRAFFIC_ANNOUNCEMENT_TYPE,
+                    /* vendorInfo= */ null);
+        });
+
+        assertWithMessage("Exception for null vendor info in announcement constructor")
+                .that(thrown).hasMessageThat().contains("Vendor info cannot be null");
+    }
+
+    @Test
+    public void getSelector() {
+        assertWithMessage("Radio announcement selector")
+                .that(TEST_ANNOUNCEMENT.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR);
+    }
+
+    @Test
+    public void getType() {
+        assertWithMessage("Radio announcement type")
+                .that(TEST_ANNOUNCEMENT.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE);
+    }
+
+    @Test
+    public void getVendorInfo() {
+        assertWithMessage("Radio announcement vendor info")
+                .that(TEST_ANNOUNCEMENT.getVendorInfo()).isEqualTo(VENDOR_INFO);
+    }
+
+    private static Map<String, String> createVendorInfo() {
+        Map<String, String> vendorInfo = new ArrayMap<>();
+        vendorInfo.put("vendorKeyMock", "vendorValueMock");
+        return vendorInfo;
+    }
+}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
index 259a118..be4d0d4 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -18,11 +18,36 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+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.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.IRadioService;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.os.RemoteException;
+import android.util.ArrayMap;
 
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(MockitoJUnitRunner.class)
 public final class RadioManagerTest {
 
     private static final int REGION = RadioManager.REGION_ITU_2;
@@ -63,6 +88,50 @@
     private static final RadioManager.AmBandConfig AM_BAND_CONFIG = createAmBandConfig();
     private static final RadioManager.ModuleProperties AMFM_PROPERTIES = createAmFmProperties();
 
+    /**
+     * Info flags with live, tuned and stereo enabled
+     */
+    private static final int INFO_FLAGS = 0b110001;
+    private static final int SIGNAL_QUALITY = 2;
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000111);
+    private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0x10000113);
+    private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1013);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 95500);
+    private static final ProgramSelector DAB_SELECTOR =
+            new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
+                    new ProgramSelector.Identifier[]{
+                            DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER},
+                    /* vendorIds= */ null);
+    private static final RadioMetadata METADATA = createMetadata();
+    private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO =
+            createDabProgramInfo(DAB_SELECTOR);
+
+    private static final int EVENT_ANNOUNCEMENT_TYPE = Announcement.TYPE_EVENT;
+    private static final List<Announcement> TEST_ANNOUNCEMENT_LIST = Arrays.asList(
+            new Announcement(DAB_SELECTOR, EVENT_ANNOUNCEMENT_TYPE,
+                    /* vendorInfo= */ new ArrayMap<>()));
+
+    private RadioManager mRadioManager;
+
+    @Mock
+    private IRadioService mRadioServiceMock;
+    @Mock
+    private Context mContextMock;
+    @Mock
+    private RadioTuner.Callback mCallbackMock;
+    @Mock
+    private Announcement.OnListUpdatedListener mEventListener;
+    @Mock
+    private ICloseHandle mCloseHandleMock;
+
     @Test
     public void getType_forBandDescriptor() {
         RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
@@ -460,6 +529,197 @@
                 .that(AMFM_PROPERTIES).isNotEqualTo(propertiesDab);
     }
 
+    @Test
+    public void getSelector_forProgramInfo() {
+        assertWithMessage("Selector of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR);
+    }
+
+    @Test
+    public void getLogicallyTunedTo_forProgramInfo() {
+        assertWithMessage("Identifier logically tuned to in DAB program info")
+                .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER);
+    }
+
+    @Test
+    public void getPhysicallyTunedTo_forProgramInfo() {
+        assertWithMessage("Identifier physically tuned to DAB program info")
+                .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER);
+    }
+
+    @Test
+    public void getRelatedContent_forProgramInfo() {
+        assertWithMessage("Related contents of DAB program info")
+                .that(DAB_PROGRAM_INFO.getRelatedContent())
+                .containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED);
+    }
+
+    @Test
+    public void getChannel_forProgramInfo() {
+        assertWithMessage("Main channel of DAB program info")
+                .that(DAB_PROGRAM_INFO.getChannel()).isEqualTo(0);
+    }
+
+    @Test
+    public void getSubChannel_forProgramInfo() {
+        assertWithMessage("Sub channel of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSubChannel()).isEqualTo(0);
+    }
+
+    @Test
+    public void isTuned_forProgramInfo() {
+        assertWithMessage("Tuned status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isTuned()).isTrue();
+    }
+
+    @Test
+    public void isStereo_forProgramInfo() {
+        assertWithMessage("Stereo support in DAB program info")
+                .that(DAB_PROGRAM_INFO.isStereo()).isTrue();
+    }
+
+    @Test
+    public void isDigital_forProgramInfo() {
+        assertWithMessage("Digital DAB program info")
+                .that(DAB_PROGRAM_INFO.isDigital()).isTrue();
+    }
+
+    @Test
+    public void isLive_forProgramInfo() {
+        assertWithMessage("Live status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isLive()).isTrue();
+    }
+
+    @Test
+    public void isMuted_forProgramInfo() {
+        assertWithMessage("Muted status of DAB program info")
+                .that(DAB_PROGRAM_INFO.isMuted()).isFalse();
+    }
+
+    @Test
+    public void isTrafficProgram_forProgramInfo() {
+        assertWithMessage("Traffic program support in DAB program info")
+                .that(DAB_PROGRAM_INFO.isTrafficProgram()).isFalse();
+    }
+
+    @Test
+    public void isTrafficAnnouncementActive_forProgramInfo() {
+        assertWithMessage("Active traffic announcement for DAB program info")
+                .that(DAB_PROGRAM_INFO.isTrafficAnnouncementActive()).isFalse();
+    }
+
+    @Test
+    public void getSignalStrength_forProgramInfo() {
+        assertWithMessage("Signal strength of DAB program info")
+                .that(DAB_PROGRAM_INFO.getSignalStrength()).isEqualTo(SIGNAL_QUALITY);
+    }
+
+    @Test
+    public void getMetadata_forProgramInfo() {
+        assertWithMessage("Metadata of DAB program info")
+                .that(DAB_PROGRAM_INFO.getMetadata()).isEqualTo(METADATA);
+    }
+
+    @Test
+    public void getVendorInfo_forProgramInfo() {
+        assertWithMessage("Vendor info of DAB program info")
+                .that(DAB_PROGRAM_INFO.getVendorInfo()).isEmpty();
+    }
+
+    @Test
+    public void equals_withSameProgramInfo_returnsTrue() {
+        RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR);
+
+        assertWithMessage("The same program info")
+                .that(dabProgramInfoCompared).isEqualTo(DAB_PROGRAM_INFO);
+    }
+
+    @Test
+    public void equals_withSameProgramInfoOfDifferentSecondaryIdSelectors_returnsFalse() {
+        ProgramSelector dabSelectorCompared = new ProgramSelector(
+                ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER,
+                new ProgramSelector.Identifier[]{DAB_FREQUENCY_IDENTIFIER},
+                /* vendorIds= */ null);
+        RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(dabSelectorCompared);
+
+        assertWithMessage("Program info with different secondary id selectors")
+                .that(DAB_PROGRAM_INFO).isNotEqualTo(dabProgramInfoCompared);
+    }
+
+    @Test
+    public void listModules_forRadioManager() throws Exception {
+        createRadioManager();
+        List<RadioManager.ModuleProperties> modules = new ArrayList<>();
+
+        mRadioManager.listModules(modules);
+
+        assertWithMessage("Modules in radio manager")
+                .that(modules).containsExactly(AMFM_PROPERTIES);
+    }
+
+    @Test
+    public void openTuner_forRadioModule() throws Exception {
+        createRadioManager();
+        int moduleId = 0;
+        boolean withAudio = true;
+
+        mRadioManager.openTuner(moduleId, FM_BAND_CONFIG, withAudio, mCallbackMock,
+                /* handler= */ null);
+
+        verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any());
+    }
+
+    @Test
+    public void addAnnouncementListener_withListenerNotAddedBefore() throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        int[] enableTypesExpected = new int[]{EVENT_ANNOUNCEMENT_TYPE};
+        ArgumentCaptor<IAnnouncementListener> announcementListener =
+                ArgumentCaptor.forClass(IAnnouncementListener.class);
+
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        verify(mRadioServiceMock).addAnnouncementListener(eq(enableTypesExpected),
+                announcementListener.capture());
+
+        announcementListener.getValue().onListUpdated(TEST_ANNOUNCEMENT_LIST);
+
+        verify(mEventListener).onListUpdated(TEST_ANNOUNCEMENT_LIST);
+    }
+
+    @Test
+    public void addAnnouncementListener_withListenerAddedBefore_closesPreviousOne()
+            throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        verify(mCloseHandleMock).close();
+    }
+
+    @Test
+    public void removeAnnouncementListener_withListenerNotAddedBefore_ignores() throws Exception {
+        createRadioManager();
+
+        mRadioManager.removeAnnouncementListener(mEventListener);
+
+        verify(mCloseHandleMock, never()).close();
+    }
+
+    @Test
+    public void removeAnnouncementListener_withListenerAddedTwice_closesTheFirstOne()
+            throws Exception {
+        createRadioManager();
+        Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE);
+        mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener);
+
+        mRadioManager.removeAnnouncementListener(mEventListener);
+
+        verify(mCloseHandleMock).close();
+    }
+
     private static RadioManager.ModuleProperties createAmFmProperties() {
         return new RadioManager.ModuleProperties(PROPERTIES_ID, SERVICE_NAME, CLASS_ID,
                 IMPLEMENTOR, PRODUCT, VERSION, SERIAL, NUM_TUNERS, NUM_AUDIO_SOURCES,
@@ -487,4 +747,26 @@
     private static RadioManager.AmBandConfig createAmBandConfig() {
         return new RadioManager.AmBandConfig(createAmBandDescriptor());
     }
+
+    private static RadioMetadata createMetadata() {
+        RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder();
+        return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest").build();
+    }
+
+    private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) {
+        return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER,
+                DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS,
+                SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null);
+    }
+
+    private void createRadioManager() throws RemoteException {
+        when(mRadioServiceMock.listModules()).thenReturn(Arrays.asList(AMFM_PROPERTIES));
+        when(mRadioServiceMock.addAnnouncementListener(any(), any())).thenReturn(mCloseHandleMock);
+
+        mRadioManager = new RadioManager(mContextMock, mRadioServiceMock);
+    }
+
+    private Set<Integer> createAnnouncementTypeSet(int enableType) {
+        return Set.of(enableType);
+    }
 }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
new file mode 100644
index 0000000..7f4ea11
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with AIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceAidlImplTest {
+
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceAidlImpl mAidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private BroadcastRadioServiceImpl mHalMock;
+    @Mock
+    private RadioManager.ModuleProperties mModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mTunerMock;
+
+    @Before
+    public void setUp() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+
+        when(mHalMock.listModules()).thenReturn(Arrays.asList(mModuleMock));
+        when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mTunerMock);
+        when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mAidlImpl = new IRadioServiceAidlImpl(mServiceMock, mHalMock);
+    }
+
+    @Test
+    public void loadModules_forAidlImpl() {
+        assertWithMessage("Modules loaded in AIDL HAL")
+                .that(mAidlImpl.listModules())
+                .containsExactly(mModuleMock);
+    }
+
+    @Test
+    public void openTuner_forAidlImpl() throws Exception {
+        ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in AIDL HAL")
+                .that(tuner).isEqualTo(mTunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forAidlImpl() {
+        ICloseHandle closeHandle = mAidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHalMock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
new file mode 100644
index 0000000..f28e27d
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.broadcastradio;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link android.hardware.radio.IRadioService} with HIDL HAL implementation
+ */
+@RunWith(MockitoJUnitRunner.class)
+public final class IRadioServiceHidlImplTest {
+
+    private static final int HAL1_MODULE_ID = 0;
+    private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+
+    private IRadioServiceHidlImpl mHidlImpl;
+
+    @Mock
+    private BroadcastRadioService mServiceMock;
+    @Mock
+    private com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1Mock;
+    @Mock
+    private com.android.server.broadcastradio.hal2.BroadcastRadioService mHal2Mock;
+    @Mock
+    private RadioManager.ModuleProperties mHal1ModuleMock;
+    @Mock
+    private RadioManager.ModuleProperties mHal2ModuleMock;
+    @Mock
+    private RadioManager.BandConfig mBandConfigMock;
+    @Mock
+    private ITunerCallback mTunerCallbackMock;
+    @Mock
+    private IAnnouncementListener mListenerMock;
+    @Mock
+    private ICloseHandle mICloseHandle;
+    @Mock
+    private ITuner mHal1TunerMock;
+    @Mock
+    private ITuner mHal2TunerMock;
+
+    @Before
+    public void setup() throws Exception {
+        doNothing().when(mServiceMock).enforcePolicyAccess();
+        when(mHal1Mock.loadModules()).thenReturn(Arrays.asList(mHal1ModuleMock));
+        when(mHal1Mock.openTuner(anyInt(), any(), anyBoolean(), any())).thenReturn(mHal1TunerMock);
+
+        when(mHal2Mock.listModules()).thenReturn(Arrays.asList(mHal2ModuleMock));
+        doAnswer(invocation -> {
+            int moduleId = (int) invocation.getArguments()[0];
+            return moduleId != HAL1_MODULE_ID;
+        }).when(mHal2Mock).hasModule(anyInt());
+        when(mHal2Mock.openSession(anyInt(), any(), anyBoolean(), any()))
+                .thenReturn(mHal2TunerMock);
+        when(mHal2Mock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
+
+        mHidlImpl = new IRadioServiceHidlImpl(mServiceMock, mHal1Mock, mHal2Mock);
+    }
+
+    @Test
+    public void loadModules_forHidlImpl() {
+        assertWithMessage("Modules loaded in HIDL HAL")
+                .that(mHidlImpl.listModules())
+                .containsExactly(mHal1ModuleMock, mHal2ModuleMock);
+    }
+
+    @Test
+    public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 1")
+                .that(tuner).isEqualTo(mHal1TunerMock);
+    }
+
+    @Test
+    public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception {
+        ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock,
+                /* withAudio= */ true, mTunerCallbackMock);
+
+        assertWithMessage("Tuner opened in HAL 2")
+                .that(tuner).isEqualTo(mHal2TunerMock);
+    }
+
+    @Test
+    public void addAnnouncementListener_forHidlImpl() {
+        when(mHal2Mock.hasAnyModules()).thenReturn(true);
+        ICloseHandle closeHandle = mHidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+
+        verify(mHal2Mock).addAnnouncementListener(ENABLE_TYPES, mListenerMock);
+        assertWithMessage("Close handle of announcement listener for HAL 2")
+                .that(closeHandle).isEqualTo(mICloseHandle);
+    }
+
+}
diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java
index 0b8b29b..bcb13d2 100644
--- a/core/tests/coretests/src/android/app/NotificationTest.java
+++ b/core/tests/coretests/src/android/app/NotificationTest.java
@@ -48,6 +48,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static junit.framework.Assert.assertNotNull;
 import static junit.framework.Assert.fail;
 
 import static org.junit.Assert.assertEquals;
@@ -56,7 +57,9 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
 import android.app.Notification.CallStyle;
@@ -68,6 +71,7 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
+import android.graphics.Typeface;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
@@ -79,7 +83,9 @@
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
 import android.text.style.TextAppearanceSpan;
+import android.util.Pair;
 import android.widget.RemoteViews;
 
 import androidx.test.InstrumentationRegistry;
@@ -89,6 +95,8 @@
 import com.android.internal.R;
 import com.android.internal.util.ContrastColorUtil;
 
+import junit.framework.Assert;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -218,8 +226,10 @@
 
     @Test
     public void allPendingIntents_recollectedAfterReusingBuilder() {
-        PendingIntent intent1 = PendingIntent.getActivity(mContext, 0, new Intent("test1"), PendingIntent.FLAG_MUTABLE_UNAUDITED);
-        PendingIntent intent2 = PendingIntent.getActivity(mContext, 0, new Intent("test2"), PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        PendingIntent intent2 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test2"), PendingIntent.FLAG_IMMUTABLE);
 
         Notification.Builder builder = new Notification.Builder(mContext, "channel");
         builder.setContentIntent(intent1);
@@ -669,30 +679,23 @@
         Notification notification = new Notification.Builder(mContext, "Channel").setStyle(
                 style).build();
 
+        int targetSize = mContext.getResources().getDimensionPixelSize(
+                ActivityManager.isLowRamDeviceStatic()
+                        ? R.dimen.notification_person_icon_max_size_low_ram
+                        : R.dimen.notification_person_icon_max_size);
+
         Bitmap personIcon = style.getUser().getIcon().getBitmap();
-        assertThat(personIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(personIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(personIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(personIcon.getHeight()).isEqualTo(targetSize);
 
         Bitmap avatarIcon = style.getMessages().get(0).getSenderPerson().getIcon().getBitmap();
-        assertThat(avatarIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(avatarIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(avatarIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(avatarIcon.getHeight()).isEqualTo(targetSize);
 
         Bitmap historicAvatarIcon = style.getHistoricMessages().get(
                 0).getSenderPerson().getIcon().getBitmap();
-        assertThat(historicAvatarIcon.getWidth()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
-        assertThat(historicAvatarIcon.getHeight()).isEqualTo(
-                mContext.getResources().getDimensionPixelSize(
-                        R.dimen.notification_person_icon_max_size));
+        assertThat(historicAvatarIcon.getWidth()).isEqualTo(targetSize);
+        assertThat(historicAvatarIcon.getHeight()).isEqualTo(targetSize);
     }
 
     @Test
@@ -780,7 +783,6 @@
         assertFalse(notification.isMediaNotification());
     }
 
-    @Test
     public void validateColorizedPaletteForColor(int rawColor) {
         Notification.Colors cDay = new Notification.Colors();
         Notification.Colors cNight = new Notification.Colors();
@@ -861,19 +863,22 @@
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_LARGE_ICON_BIG, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
 
         // no crash, good
     }
 
     @Test
     public void testRestoreFromExtras_Messaging_invalidExtra_noCrash() {
-        Notification.Style style = new Notification.MessagingStyle();
+        Notification.Style style = new Notification.MessagingStyle("test");
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_MESSAGING_PERSON, new Bundle());
         fakeTypes.putParcelable(EXTRA_CONVERSATION_ICON, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
 
         // no crash, good
     }
@@ -885,22 +890,33 @@
         fakeTypes.putParcelable(EXTRA_MEDIA_SESSION, new Bundle());
         fakeTypes.putParcelable(EXTRA_MEDIA_REMOTE_INTENT, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
 
         // no crash, good
     }
 
     @Test
     public void testRestoreFromExtras_Call_invalidExtra_noCrash() {
-        Notification.Style style = new CallStyle();
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Notification.Style style = Notification.CallStyle.forIncomingCall(
+                new Person.Builder().setName("hi").build(), intent1, intent1);
+
         Bundle fakeTypes = new Bundle();
         fakeTypes.putParcelable(EXTRA_CALL_PERSON, new Bundle());
         fakeTypes.putParcelable(EXTRA_ANSWER_INTENT, new Bundle());
         fakeTypes.putParcelable(EXTRA_DECLINE_INTENT, new Bundle());
         fakeTypes.putParcelable(EXTRA_HANG_UP_INTENT, new Bundle());
 
-        style.restoreFromExtras(fakeTypes);
-
+        Notification n = new Notification.Builder(mContext, "test")
+                .setStyle(style)
+                .setExtras(fakeTypes)
+                .build();
+        Notification.Builder.recoverBuilder(mContext, n);
         // no crash, good
     }
 
@@ -962,7 +978,11 @@
         fakeTypes.putParcelable(KEY_ON_READ, new Bundle());
         fakeTypes.putParcelable(KEY_ON_REPLY, new Bundle());
         fakeTypes.putParcelable(KEY_REMOTE_INPUT, new Bundle());
-        Notification.CarExtender.UnreadConversation.getUnreadConversationFromBundle(fakeTypes);
+
+        Notification n = new Notification.Builder(mContext, "test")
+                .setExtras(fakeTypes)
+                .build();
+        Notification.CarExtender extender = new Notification.CarExtender(n);
 
         // no crash, good
     }
@@ -980,6 +1000,493 @@
         // no crash, good
     }
 
+
+    @Test
+    public void testDoesNotStripsExtenders() {
+        Notification.Builder nb = new Notification.Builder(mContext, "channel");
+        nb.extend(new Notification.CarExtender().setColor(Color.RED));
+        nb.extend(new Notification.TvExtender().setChannelId("different channel"));
+        nb.extend(new Notification.WearableExtender().setDismissalId("dismiss"));
+        Notification before = nb.build();
+        Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before);
+
+        assertTrue(before == after);
+
+        Assert.assertEquals("different channel",
+                new Notification.TvExtender(before).getChannelId());
+        Assert.assertEquals(Color.RED, new Notification.CarExtender(before).getColor());
+        Assert.assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId());
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_noStyles() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test");
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+
+        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_noStyleToStyle() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test");
+        Notification.Builder n2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_styleToNoStyle() {
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testStyleChangeVisiblyDifferent_changeStyle() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle());
+        Notification.Builder n2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle());
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testInboxTextChange() {
+        Notification.Builder nInbox1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle().addLine("a").addLine("b"));
+        Notification.Builder nInbox2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.InboxStyle().addLine("b").addLine("c"));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2));
+    }
+
+    @Test
+    public void testBigTextTextChange() {
+        Notification.Builder nBigText1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle().bigText("something"));
+        Notification.Builder nBigText2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigTextStyle().bigText("else"));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2));
+    }
+
+    @Test
+    public void testBigPictureChange() {
+        Bitmap bitA = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+        Bitmap bitB = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
+
+        Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigPictureStyle().bigPicture(bitA));
+        Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.BigPictureStyle().bigPicture(bitB));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2));
+    }
+
+    @Test
+    public void testMessagingChange_text() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build()))
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "b", 100, new Person.Builder().setName("hi").build()))
+                );
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_data() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())
+                                .setData("text", mock(Uri.class))));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_sender() {
+        Person a = new Person.Builder().setName("A").build();
+        Person b = new Person.Builder().setName("b").build();
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_key() {
+        Person a = new Person.Builder().setName("hi").setKey("A").build();
+        Person b = new Person.Builder().setName("hi").setKey("b").build();
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
+
+        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testMessagingChange_ignoreTimeChange() {
+        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 100, new Person.Builder().setName("hi").build())));
+        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
+                .setStyle(new Notification.MessagingStyle("")
+                        .addMessage(new Notification.MessagingStyle.Message(
+                                "a", 1000, new Person.Builder().setName("hi").build()))
+                );
+
+        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
+    }
+
+    @Test
+    public void testRemoteViews_nullChange() {
+        Notification.Builder n1 = new Notification.Builder(mContext, "test")
+                .setContent(mock(RemoteViews.class));
+        Notification.Builder n2 = new Notification.Builder(mContext, "test");
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test")
+                .setContent(mock(RemoteViews.class));
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test")
+                .setCustomBigContentView(mock(RemoteViews.class));
+        n2 = new Notification.Builder(mContext, "test");
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test")
+                .setCustomBigContentView(mock(RemoteViews.class));
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test");
+        n2 = new Notification.Builder(mContext, "test");
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_layoutChange() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(189);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_layoutSame() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_sequenceChange() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        when(a.getSequenceNumber()).thenReturn(1);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+        when(b.getSequenceNumber()).thenReturn(2);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testRemoteViews_sequenceSame() {
+        RemoteViews a = mock(RemoteViews.class);
+        when(a.getLayoutId()).thenReturn(234);
+        when(a.getSequenceNumber()).thenReturn(1);
+        RemoteViews b = mock(RemoteViews.class);
+        when(b.getLayoutId()).thenReturn(234);
+        when(b.getSequenceNumber()).thenReturn(1);
+
+        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
+        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+
+        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
+        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
+        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferent_null() {
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentSame() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentText() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
+                .build();
+
+        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentSpannables() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon,
+                        new SpannableStringBuilder().append("test1",
+                                new StyleSpan(Typeface.BOLD),
+                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE),
+                        intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "test1", intent).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentNumber() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
+                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
+                .build();
+
+        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsDifferentIntent() {
+        PendingIntent intent1 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        PendingIntent intent2 = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testActionsIgnoresRemoteInputs() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        Notification n1 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(new RemoteInput.Builder("a")
+                                .setChoices(new CharSequence[] {"i", "m"})
+                                .build())
+                        .build())
+                .build();
+        Notification n2 = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(new RemoteInput.Builder("a")
+                                .setChoices(new CharSequence[] {"t", "m"})
+                                .build())
+                        .build())
+                .build();
+
+        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_noRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .build())
+                .build();
+        Assert.assertNull(notification.findRemoteInputActionPair(false));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_hasRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        RemoteInput remoteInput = new RemoteInput.Builder("a").build();
+
+        Notification.Action actionWithRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(remoteInput)
+                        .addRemoteInput(remoteInput)
+                        .build();
+
+        Notification.Action actionWithoutRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 2", intent)
+                        .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(actionWithoutRemoteInput)
+                .addAction(actionWithRemoteInput)
+                .build();
+
+        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+                notification.findRemoteInputActionPair(false);
+
+        assertNotNull(remoteInputActionPair);
+        Assert.assertEquals(remoteInput, remoteInputActionPair.first);
+        Assert.assertEquals(actionWithRemoteInput, remoteInputActionPair.second);
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(
+                                new RemoteInput.Builder("a")
+                                        .setAllowFreeFormInput(false).build())
+                        .build())
+                .build();
+        Assert.assertNull(notification.findRemoteInputActionPair(true));
+    }
+
+    @Test
+    public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() {
+        PendingIntent intent = PendingIntent.getActivity(
+                mContext, 0, new Intent("test1"), PendingIntent.FLAG_IMMUTABLE);;
+        Icon icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888));
+
+        RemoteInput remoteInput =
+                new RemoteInput.Builder("a").setAllowFreeFormInput(false).build();
+        RemoteInput freeformRemoteInput =
+                new RemoteInput.Builder("b").setAllowFreeFormInput(true).build();
+
+        Notification.Action actionWithFreeformRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 1", intent)
+                        .addRemoteInput(remoteInput)
+                        .addRemoteInput(freeformRemoteInput)
+                        .build();
+
+        Notification.Action actionWithoutFreeformRemoteInput =
+                new Notification.Action.Builder(icon, "TEXT 2", intent)
+                        .addRemoteInput(remoteInput)
+                        .build();
+
+        Notification notification = new Notification.Builder(mContext, "test")
+                .addAction(actionWithoutFreeformRemoteInput)
+                .addAction(actionWithFreeformRemoteInput)
+                .build();
+
+        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+                notification.findRemoteInputActionPair(true);
+
+        assertNotNull(remoteInputActionPair);
+        Assert.assertEquals(freeformRemoteInput, remoteInputActionPair.first);
+        Assert.assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
+    }
+
     private void assertValid(Notification.Colors c) {
         // Assert that all colors are populated
         assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID);
diff --git a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java b/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java
deleted file mode 100644
index 7c7cd12..0000000
--- a/core/tests/coretests/src/android/app/time/TimeConfigurationTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.app.time;
-
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class TimeConfigurationTest {
-
-    @Test
-    public void testBuilder() {
-        TimeConfiguration first = new TimeConfiguration.Builder()
-                .setAutoDetectionEnabled(true)
-                .build();
-
-        assertThat(first.isAutoDetectionEnabled()).isTrue();
-
-        TimeConfiguration copyFromBuilderConfiguration = new TimeConfiguration.Builder(first)
-                .build();
-
-        assertThat(first).isEqualTo(copyFromBuilderConfiguration);
-    }
-
-    @Test
-    public void testParcelable() {
-        TimeConfiguration.Builder builder = new TimeConfiguration.Builder();
-
-        assertRoundTripParcelable(builder.setAutoDetectionEnabled(true).build());
-
-        assertRoundTripParcelable(builder.setAutoDetectionEnabled(false).build());
-    }
-
-}
diff --git a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
index 3ab01f3..e7d352c 100644
--- a/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
+++ b/core/tests/coretests/src/android/app/time/UnixEpochTimeTest.java
@@ -16,11 +16,9 @@
 
 package android.app.time;
 
-import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 
 import android.os.ShellCommand;
 
@@ -31,35 +29,12 @@
 
 /**
  * Tests for non-SDK methods on {@link UnixEpochTime}.
+ *
+ * <p>See also {@link android.app.time.cts.UnixEpochTimeTest} for SDK methods.
  */
 @RunWith(AndroidJUnit4.class)
 public class UnixEpochTimeTest {
 
-    @Test
-    public void testEqualsAndHashcode() {
-        UnixEpochTime one1000one = new UnixEpochTime(1000, 1);
-        assertEqualsAndHashCode(one1000one, one1000one);
-
-        UnixEpochTime one1000two = new UnixEpochTime(1000, 1);
-        assertEqualsAndHashCode(one1000one, one1000two);
-
-        UnixEpochTime two1000 = new UnixEpochTime(1000, 2);
-        assertNotEquals(one1000one, two1000);
-
-        UnixEpochTime one2000 = new UnixEpochTime(2000, 1);
-        assertNotEquals(one1000one, one2000);
-    }
-
-    private static void assertEqualsAndHashCode(Object one, Object two) {
-        assertEquals(one, two);
-        assertEquals(one.hashCode(), two.hashCode());
-    }
-
-    @Test
-    public void testParceling() {
-        assertRoundTripParcelable(new UnixEpochTime(1000, 1));
-    }
-
     @Test(expected = IllegalArgumentException.class)
     public void testParseCommandLineArg_noElapsedRealtime() {
         ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
@@ -91,22 +66,6 @@
     }
 
     @Test
-    public void testAt() {
-        long timeMillis = 1000L;
-        int elapsedRealtimeMillis = 100;
-        UnixEpochTime unixEpochTime = new UnixEpochTime(elapsedRealtimeMillis, timeMillis);
-        // Reference time is after the timestamp.
-        UnixEpochTime at125 = unixEpochTime.at(125);
-        assertEquals(timeMillis + (125 - elapsedRealtimeMillis), at125.getUnixEpochTimeMillis());
-        assertEquals(125, at125.getElapsedRealtimeMillis());
-
-        // Reference time is before the timestamp.
-        UnixEpochTime at75 = unixEpochTime.at(75);
-        assertEquals(timeMillis + (75 - elapsedRealtimeMillis), at75.getUnixEpochTimeMillis());
-        assertEquals(75, at75.getElapsedRealtimeMillis());
-    }
-
-    @Test
     public void testElapsedRealtimeDifference() {
         UnixEpochTime value1 = new UnixEpochTime(1000, 123L);
         assertEquals(0, UnixEpochTime.elapsedRealtimeDifference(value1, value1));
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index fa4952e1..5553902 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -25,11 +25,12 @@
 import android.test.AndroidTestCase;
 import android.util.AttributeSet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import androidx.test.filters.LargeTest;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.ByteArrayInputStream;
diff --git a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
index e750454..1c7ab74 100644
--- a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
+++ b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
@@ -23,13 +23,14 @@
 
 import android.os.Parcel;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
index c8de190..ab63f14 100644
--- a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
+++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
@@ -17,6 +17,9 @@
 package android.service.timezone;
 
 import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -33,6 +36,32 @@
 
     @Test
     public void isEquivalentToAndEquals() {
+        long creationElapsedMillis = 1111L;
+        TimeZoneProviderEvent failEvent =
+                TimeZoneProviderEvent.createPermanentFailureEvent(creationElapsedMillis, "one");
+        TimeZoneProviderStatus providerStatus = TimeZoneProviderStatus.UNKNOWN;
+
+        TimeZoneProviderEvent uncertainEvent =
+                TimeZoneProviderEvent.createUncertainEvent(creationElapsedMillis, providerStatus);
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(creationElapsedMillis)
+                .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                .build();
+        TimeZoneProviderEvent suggestionEvent = TimeZoneProviderEvent.createSuggestionEvent(
+                creationElapsedMillis, suggestion, providerStatus);
+
+        assertNotEquals(failEvent, uncertainEvent);
+        assertNotEquivalentTo(failEvent, uncertainEvent);
+
+        assertNotEquals(failEvent, suggestionEvent);
+        assertNotEquivalentTo(failEvent, suggestionEvent);
+
+        assertNotEquals(uncertainEvent, suggestionEvent);
+        assertNotEquivalentTo(uncertainEvent, suggestionEvent);
+    }
+
+    @Test
+    public void isEquivalentToAndEquals_permanentFailure() {
         TimeZoneProviderEvent fail1v1 =
                 TimeZoneProviderEvent.createPermanentFailureEvent(1111L, "one");
         assertEquals(fail1v1, fail1v1);
@@ -51,44 +80,79 @@
             assertNotEquals(fail1v1, fail2);
             assertIsEquivalentTo(fail1v1, fail2);
         }
+    }
 
-        TimeZoneProviderEvent uncertain1v1 = TimeZoneProviderEvent.createUncertainEvent(1111L);
+    @Test
+    public void isEquivalentToAndEquals_uncertain() {
+        TimeZoneProviderStatus status1 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING)
+                .build();
+        TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED)
+                .build();
+
+        TimeZoneProviderEvent uncertain1v1 =
+                TimeZoneProviderEvent.createUncertainEvent(1111L, status1);
         assertEquals(uncertain1v1, uncertain1v1);
         assertIsEquivalentTo(uncertain1v1, uncertain1v1);
         assertNotEquals(uncertain1v1, null);
         assertNotEquivalentTo(uncertain1v1, null);
 
         {
-            TimeZoneProviderEvent uncertain1v2 = TimeZoneProviderEvent.createUncertainEvent(1111L);
+            TimeZoneProviderEvent uncertain1v2 =
+                    TimeZoneProviderEvent.createUncertainEvent(1111L, status1);
             assertEquals(uncertain1v1, uncertain1v2);
             assertIsEquivalentTo(uncertain1v1, uncertain1v2);
 
-            TimeZoneProviderEvent uncertain2 = TimeZoneProviderEvent.createUncertainEvent(2222L);
+            TimeZoneProviderEvent uncertain2 =
+                    TimeZoneProviderEvent.createUncertainEvent(2222L, status1);
             assertNotEquals(uncertain1v1, uncertain2);
             assertIsEquivalentTo(uncertain1v1, uncertain2);
-        }
 
+            TimeZoneProviderEvent uncertain3 =
+                    TimeZoneProviderEvent.createUncertainEvent(1111L, status2);
+            assertNotEquals(uncertain1v1, uncertain3);
+            assertNotEquivalentTo(uncertain1v1, uncertain3);
+        }
+    }
+
+    @Test
+    public void isEquivalentToAndEquals_suggestion() {
+        TimeZoneProviderStatus status1 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING)
+                .build();
+        TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED)
+                .build();
         TimeZoneProviderSuggestion suggestion1 = new TimeZoneProviderSuggestion.Builder()
                 .setElapsedRealtimeMillis(1111L)
                 .setTimeZoneIds(Collections.singletonList("Europe/London"))
                 .build();
         TimeZoneProviderEvent certain1v1 =
-                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1);
+                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1, status1);
         assertEquals(certain1v1, certain1v1);
         assertIsEquivalentTo(certain1v1, certain1v1);
         assertNotEquals(certain1v1, null);
         assertNotEquivalentTo(certain1v1, null);
 
         {
-            // Same suggestion, same time.
+            // Same time, suggestion, and status.
             TimeZoneProviderEvent certain1v2 =
-                    TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1);
+                    TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1, status1);
             assertEquals(certain1v1, certain1v2);
             assertIsEquivalentTo(certain1v1, certain1v2);
 
-            // Same suggestion, different time.
+            // Different time, same suggestion and status.
             TimeZoneProviderEvent certain1v3 =
-                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1);
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1, status1);
             assertNotEquals(certain1v1, certain1v3);
             assertIsEquivalentTo(certain1v1, certain1v3);
 
@@ -100,7 +164,7 @@
             assertNotEquals(suggestion1, suggestion2);
             TimeZoneProviderSuggestionTest.assertIsEquivalentTo(suggestion1, suggestion2);
             TimeZoneProviderEvent certain2 =
-                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2);
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2, status1);
             assertNotEquals(certain1v1, certain2);
             assertIsEquivalentTo(certain1v1, certain2);
 
@@ -109,16 +173,15 @@
                     .setTimeZoneIds(Collections.singletonList("Europe/Paris"))
                     .build();
             TimeZoneProviderEvent certain3 =
-                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3);
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3, status1);
             assertNotEquals(certain1v1, certain3);
             assertNotEquivalentTo(certain1v1, certain3);
+
+            TimeZoneProviderEvent certain4 =
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1, status2);
+            assertNotEquals(certain1v1, certain4);
+            assertNotEquivalentTo(certain1v1, certain4);
         }
-
-        assertNotEquals(fail1v1, uncertain1v1);
-        assertNotEquivalentTo(fail1v1, uncertain1v1);
-
-        assertNotEquals(fail1v1, certain1v1);
-        assertNotEquivalentTo(fail1v1, certain1v1);
     }
 
     @Test
@@ -130,7 +193,8 @@
 
     @Test
     public void testParcelable_uncertain() {
-        TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(1111L);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(
+                1111L, TimeZoneProviderStatus.UNKNOWN);
         assertRoundTripParcelable(event);
     }
 
@@ -139,8 +203,8 @@
         TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
                 .setTimeZoneIds(Arrays.asList("Europe/London", "Europe/Paris"))
                 .build();
-        TimeZoneProviderEvent event =
-                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                1111L, suggestion, TimeZoneProviderStatus.UNKNOWN);
         assertRoundTripParcelable(event);
     }
 
diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java
new file mode 100644
index 0000000..d61c33c
--- /dev/null
+++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderStatusTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone;
+
+import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+public class TimeZoneProviderStatusTest {
+
+    @Test
+    public void testStatusValidation() {
+        TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(DEPENDENCY_STATUS_WORKING)
+                .build();
+
+        assertThrows(IllegalArgumentException.class,
+                () -> new TimeZoneProviderStatus.Builder(status)
+                        .setLocationDetectionStatus(-1)
+                        .build());
+        assertThrows(IllegalArgumentException.class,
+                () -> new TimeZoneProviderStatus.Builder(status)
+                        .setConnectivityStatus(-1)
+                        .build());
+        assertThrows(IllegalArgumentException.class,
+                () -> new TimeZoneProviderStatus.Builder(status)
+                        .setTimeZoneResolutionStatus(-1)
+                        .build());
+    }
+
+    @Test
+    public void testEqualsAndHashcode() {
+        TimeZoneProviderStatus status1_1 = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING)
+                .build();
+        assertEqualsAndHashcode(status1_1, status1_1);
+        assertNotEquals(status1_1, null);
+
+        {
+            TimeZoneProviderStatus status1_2 =
+                    new TimeZoneProviderStatus.Builder(status1_1).build();
+            assertEqualsAndHashcode(status1_1, status1_2);
+            assertNotSame(status1_1, status1_2);
+        }
+
+        {
+            TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1)
+                    .setLocationDetectionStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT)
+                    .build();
+            assertNotEquals(status1_1, status2);
+        }
+
+        {
+            TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1)
+                    .setConnectivityStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT)
+                    .build();
+            assertNotEquals(status1_1, status2);
+        }
+
+        {
+            TimeZoneProviderStatus status2 = new TimeZoneProviderStatus.Builder(status1_1)
+                    .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED)
+                    .build();
+            assertNotEquals(status1_1, status2);
+        }
+    }
+
+    private static void assertEqualsAndHashcode(Object one, Object two) {
+        assertEquals(one, two);
+        assertEquals(two, one);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+
+    @Test
+    public void testParcelable() {
+        TimeZoneProviderStatus status = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED)
+                .build();
+        assertRoundTripParcelable(status);
+    }
+}
diff --git a/core/tests/coretests/src/android/util/BinaryXmlTest.java b/core/tests/coretests/src/android/util/BinaryXmlTest.java
index fd625dce..025e831 100644
--- a/core/tests/coretests/src/android/util/BinaryXmlTest.java
+++ b/core/tests/coretests/src/android/util/BinaryXmlTest.java
@@ -30,6 +30,9 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/core/tests/coretests/src/android/util/XmlTest.java b/core/tests/coretests/src/android/util/XmlTest.java
index 1cd4d13..91ebc2a 100644
--- a/core/tests/coretests/src/android/util/XmlTest.java
+++ b/core/tests/coretests/src/android/util/XmlTest.java
@@ -29,6 +29,8 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
index f5fcb03..297b07f 100644
--- a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
+++ b/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java
@@ -20,8 +20,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
 
@@ -33,6 +35,7 @@
 
 import java.util.Locale;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -41,28 +44,28 @@
     public void verifyLocale(final String localeString) {
         // InputMethodSubtype#getLocale() returns exactly the same string that is passed to the
         // constructor.
-        assertEquals(localeString, createDummySubtype(localeString).getLocale());
+        assertEquals(localeString, createSubtype(localeString).getLocale());
 
         // InputMethodSubtype#getLocale() should be preserved via marshaling.
-        assertEquals(createDummySubtype(localeString).getLocale(),
-                cloneViaParcel(createDummySubtype(localeString)).getLocale());
+        assertEquals(createSubtype(localeString).getLocale(),
+                cloneViaParcel(createSubtype(localeString)).getLocale());
 
         // InputMethodSubtype#getLocale() should be preserved via marshaling.
-        assertEquals(createDummySubtype(localeString).getLocale(),
-                cloneViaParcel(cloneViaParcel(createDummySubtype(localeString))).getLocale());
+        assertEquals(createSubtype(localeString).getLocale(),
+                cloneViaParcel(cloneViaParcel(createSubtype(localeString))).getLocale());
 
         // Make sure InputMethodSubtype#hashCode() returns the same hash code.
-        assertEquals(createDummySubtype(localeString).hashCode(),
-                createDummySubtype(localeString).hashCode());
-        assertEquals(createDummySubtype(localeString).hashCode(),
-                cloneViaParcel(createDummySubtype(localeString)).hashCode());
-        assertEquals(createDummySubtype(localeString).hashCode(),
-                cloneViaParcel(cloneViaParcel(createDummySubtype(localeString))).hashCode());
+        assertEquals(createSubtype(localeString).hashCode(),
+                createSubtype(localeString).hashCode());
+        assertEquals(createSubtype(localeString).hashCode(),
+                cloneViaParcel(createSubtype(localeString)).hashCode());
+        assertEquals(createSubtype(localeString).hashCode(),
+                cloneViaParcel(cloneViaParcel(createSubtype(localeString))).hashCode());
     }
 
     @Test
     public void testLocaleObj_locale() {
-        final InputMethodSubtype usSubtype = createDummySubtype("en_US");
+        final InputMethodSubtype usSubtype = createSubtype("en_US");
         Locale localeObject = usSubtype.getLocaleObject();
         assertEquals("en", localeObject.getLanguage());
         assertEquals("US", localeObject.getCountry());
@@ -73,7 +76,7 @@
 
     @Test
     public void testLocaleObj_languageTag() {
-        final InputMethodSubtype usSubtype = createDummySubtypeUsingLanguageTag("en-US");
+        final InputMethodSubtype usSubtype = createSubtypeUsingLanguageTag("en-US");
         Locale localeObject = usSubtype.getLocaleObject();
         assertNotNull(localeObject);
         assertEquals("en", localeObject.getLanguage());
@@ -85,7 +88,7 @@
 
     @Test
     public void testLocaleObj_emptyLocale() {
-        final InputMethodSubtype emptyLocaleSubtype = createDummySubtype("");
+        final InputMethodSubtype emptyLocaleSubtype = createSubtype("");
         assertNull(emptyLocaleSubtype.getLocaleObject());
         // It should continue returning null when called multiple times.
         assertNull(emptyLocaleSubtype.getLocaleObject());
@@ -110,8 +113,8 @@
     @Test
     public void testDeprecatedLocaleString() throws Exception {
         // Make sure "iw" is not automatically replaced with "he".
-        final InputMethodSubtype subtypeIw = createDummySubtype("iw");
-        final InputMethodSubtype subtypeHe = createDummySubtype("he");
+        final InputMethodSubtype subtypeIw = createSubtype("iw");
+        final InputMethodSubtype subtypeHe = createSubtype("he");
         assertEquals("iw", subtypeIw.getLocale());
         assertEquals("he", subtypeHe.getLocale());
         assertFalse(Objects.equals(subtypeIw, subtypeHe));
@@ -125,6 +128,64 @@
         assertEquals("he", clonedSubtypeHe.getLocale());
     }
 
+    @Test
+    public void testCanonicalizedLanguageTagObjectCache() {
+        final InputMethodSubtype subtype = createSubtypeUsingLanguageTag("en-US");
+        // Verify that the returned object is cached and any subsequent call should return the same
+        // object, which is strictly guaranteed if the method gets called only on a single thread.
+        assertSame(subtype.getCanonicalizedLanguageTag(), subtype.getCanonicalizedLanguageTag());
+    }
+
+    @Test
+    public void testCanonicalizedLanguageTag() {
+        verifyCanonicalizedLanguageTag("en", "en");
+        verifyCanonicalizedLanguageTag("en-US", "en-US");
+        verifyCanonicalizedLanguageTag("en-Latn-US-t-k0-qwerty", "en-Latn-US-t-k0-qwerty");
+
+        verifyCanonicalizedLanguageTag("en-us", "en-US");
+        verifyCanonicalizedLanguageTag("EN-us", "en-US");
+
+        verifyCanonicalizedLanguageTag(null, "");
+        verifyCanonicalizedLanguageTag("", "");
+
+        verifyCanonicalizedLanguageTag("und", "und");
+        verifyCanonicalizedLanguageTag("apparently invalid language tag!!!", "und");
+    }
+
+    private void verifyCanonicalizedLanguageTag(
+            @Nullable String languageTag, @Nullable String expectedLanguageTag) {
+        final InputMethodSubtype subtype = createSubtypeUsingLanguageTag(languageTag);
+        assertEquals(subtype.getCanonicalizedLanguageTag(), expectedLanguageTag);
+    }
+
+    @Test
+    public void testIsSuitableForPhysicalKeyboardLayoutMapping() {
+        final Supplier<InputMethodSubtypeBuilder> getValidBuilder = () ->
+                new InputMethodSubtypeBuilder()
+                        .setLanguageTag("en-US")
+                        .setIsAuxiliary(false)
+                        .setSubtypeMode("keyboard")
+                        .setSubtypeId(1);
+
+        assertTrue(getValidBuilder.get().build().isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // mode == "voice" is not suitable.
+        assertFalse(getValidBuilder.get().setSubtypeMode("voice").build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // Auxiliary subtype not suitable.
+        assertFalse(getValidBuilder.get().setIsAuxiliary(true).build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // languageTag == null is not suitable.
+        assertFalse(getValidBuilder.get().setLanguageTag(null).build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+
+        // languageTag == "und" is not suitable.
+        assertFalse(getValidBuilder.get().setLanguageTag("und").build()
+                .isSuitableForPhysicalKeyboardLayoutMapping());
+    }
+
     private static InputMethodSubtype cloneViaParcel(final InputMethodSubtype original) {
         Parcel parcel = null;
         try {
@@ -139,7 +200,7 @@
         }
     }
 
-    private static InputMethodSubtype createDummySubtype(final String locale) {
+    private static InputMethodSubtype createSubtype(final String locale) {
         final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
         return builder.setSubtypeNameResId(0)
                 .setSubtypeIconResId(0)
@@ -148,7 +209,7 @@
                 .build();
     }
 
-    private static InputMethodSubtype createDummySubtypeUsingLanguageTag(
+    private static InputMethodSubtype createSubtypeUsingLanguageTag(
             final String languageTag) {
         final InputMethodSubtypeBuilder builder = new InputMethodSubtypeBuilder();
         return builder.setSubtypeNameResId(0)
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index f448cb3..f370ebd 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -60,6 +60,8 @@
     private OnBackAnimationCallback mCallback1;
     @Mock
     private OnBackAnimationCallback mCallback2;
+    @Mock
+    private BackEvent mBackEvent;
 
     @Before
     public void setUp() throws Exception {
@@ -85,14 +87,14 @@
         verify(mWindowSession, times(2)).setOnBackInvokedCallbackInfo(
                 Mockito.eq(mWindow),
                 captor.capture());
-        captor.getAllValues().get(0).getCallback().onBackStarted();
+        captor.getAllValues().get(0).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted();
+        verify(mCallback1).onBackStarted(mBackEvent);
         verifyZeroInteractions(mCallback2);
 
-        captor.getAllValues().get(1).getCallback().onBackStarted();
+        captor.getAllValues().get(1).getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted();
+        verify(mCallback2).onBackStarted(mBackEvent);
         verifyNoMoreInteractions(mCallback1);
     }
 
@@ -110,9 +112,9 @@
                 Mockito.eq(mWindow), captor.capture());
         verifyNoMoreInteractions(mWindowSession);
         assertEquals(captor.getValue().getPriority(), OnBackInvokedDispatcher.PRIORITY_OVERLAY);
-        captor.getValue().getCallback().onBackStarted();
+        captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback1).onBackStarted();
+        verify(mCallback1).onBackStarted(mBackEvent);
     }
 
     @Test
@@ -148,8 +150,8 @@
         mDispatcher.registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_OVERLAY, mCallback2);
         verify(mWindowSession).setOnBackInvokedCallbackInfo(Mockito.eq(mWindow), captor.capture());
-        captor.getValue().getCallback().onBackStarted();
+        captor.getValue().getCallback().onBackStarted(mBackEvent);
         waitForIdle();
-        verify(mCallback2).onBackStarted();
+        verify(mCallback2).onBackStarted(mBackEvent);
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/util/FastDataTest.java b/core/tests/coretests/src/com/android/internal/util/FastDataTest.java
index 04dfd6e..de325ab 100644
--- a/core/tests/coretests/src/com/android/internal/util/FastDataTest.java
+++ b/core/tests/coretests/src/com/android/internal/util/FastDataTest.java
@@ -23,6 +23,9 @@
 import android.annotation.NonNull;
 import android.util.ExceptionUtils;
 
+import com.android.modules.utils.FastDataInput;
+import com.android.modules.utils.FastDataOutput;
+
 import libcore.util.HexEncoding;
 
 import org.junit.Assume;
@@ -39,6 +42,8 @@
 import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
@@ -61,14 +66,32 @@
         this.use4ByteSequence = use4ByteSequence;
     }
 
+    @NonNull
+    private FastDataInput createFastDataInput(@NonNull InputStream in, int bufferSize) {
+        if (use4ByteSequence) {
+            return new ArtFastDataInput(in, bufferSize);
+        } else {
+            return new FastDataInput(in, bufferSize);
+        }
+    }
+
+    @NonNull
+    private FastDataOutput createFastDataOutput(@NonNull OutputStream out, int bufferSize) {
+        if (use4ByteSequence) {
+            return new ArtFastDataOutput(out, bufferSize);
+        } else {
+            return new FastDataOutput(out, bufferSize);
+        }
+    }
+
     @Test
     public void testEndOfFile_Int() throws Exception {
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1 }), 1000)) {
             assertThrows(EOFException.class, () -> in.readInt());
         }
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             assertEquals(1, in.readByte());
             assertThrows(EOFException.class, () -> in.readInt());
         }
@@ -76,25 +99,25 @@
 
     @Test
     public void testEndOfFile_String() throws Exception {
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1 }), 1000)) {
             assertThrows(EOFException.class, () -> in.readUTF());
         }
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             assertThrows(EOFException.class, () -> in.readUTF());
         }
     }
 
     @Test
     public void testEndOfFile_Bytes_Small() throws Exception {
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             final byte[] tmp = new byte[10];
             assertThrows(EOFException.class, () -> in.readFully(tmp));
         }
-        try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
-                new byte[] { 1, 1, 1, 1 }), 1000, use4ByteSequence)) {
+        try (FastDataInput in = createFastDataInput(new ByteArrayInputStream(
+                new byte[] { 1, 1, 1, 1 }), 1000)) {
             final byte[] tmp = new byte[10_000];
             assertThrows(EOFException.class, () -> in.readFully(tmp));
         }
@@ -103,8 +126,7 @@
     @Test
     public void testUTF_Bounds() throws Exception {
         final char[] buf = new char[65_534];
-        try (FastDataOutput out = new FastDataOutput(new ByteArrayOutputStream(),
-                BOUNCE_SIZE, use4ByteSequence)) {
+        try (FastDataOutput out = createFastDataOutput(new ByteArrayOutputStream(), BOUNCE_SIZE)) {
             // Writing simple string will fit fine
             Arrays.fill(buf, '!');
             final String simple = new String(buf);
@@ -132,17 +154,15 @@
             doTranscodeWrite(out);
             out.flush();
 
-            final FastDataInput in = new FastDataInput(
-                    new ByteArrayInputStream(outStream.toByteArray()),
-                    BOUNCE_SIZE, use4ByteSequence);
+            final FastDataInput in = createFastDataInput(
+                    new ByteArrayInputStream(outStream.toByteArray()), BOUNCE_SIZE);
             doTranscodeRead(in);
         }
 
         // Verify that fast data can be read by upstream
         {
             final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-            final FastDataOutput out = new FastDataOutput(outStream,
-                    BOUNCE_SIZE, use4ByteSequence);
+            final FastDataOutput out = createFastDataOutput(outStream, BOUNCE_SIZE);
             doTranscodeWrite(out);
             out.flush();
 
@@ -299,7 +319,7 @@
         final DataOutput slowData = new DataOutputStream(slowStream);
 
         final ByteArrayOutputStream fastStream = new ByteArrayOutputStream();
-        final FastDataOutput fastData = FastDataOutput.obtainUsing3ByteSequences(fastStream);
+        final FastDataOutput fastData = FastDataOutput.obtain(fastStream);
 
         for (int cp = Character.MIN_CODE_POINT; cp < Character.MAX_CODE_POINT; cp++) {
             if (Character.isValidCodePoint(cp)) {
@@ -416,16 +436,14 @@
     private void doBounce(@NonNull ThrowingConsumer<FastDataOutput> out,
             @NonNull ThrowingConsumer<FastDataInput> in, int count) throws Exception {
         final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-        final FastDataOutput outData = new FastDataOutput(outStream,
-                BOUNCE_SIZE, use4ByteSequence);
+        final FastDataOutput outData = createFastDataOutput(outStream, BOUNCE_SIZE);
         for (int i = 0; i < count; i++) {
             out.accept(outData);
         }
         outData.flush();
 
         final ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
-        final FastDataInput inData = new FastDataInput(inStream,
-                BOUNCE_SIZE, use4ByteSequence);
+        final FastDataInput inData = createFastDataInput(inStream, BOUNCE_SIZE);
         for (int i = 0; i < count; i++) {
             in.accept(inData);
         }
diff --git a/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java
index 06e9759..0484068 100644
--- a/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/XmlUtilsTest.java
@@ -18,10 +18,11 @@
 
 import static org.junit.Assert.assertArrayEquals;
 
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import junit.framework.TestCase;
 
 import java.io.ByteArrayInputStream;
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 699e794..decfb9f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -265,6 +265,7 @@
         <permission name="android.permission.INSTALL_LOCATION_PROVIDER"/>
         <permission name="android.permission.INSTALL_PACKAGES"/>
         <permission name="android.permission.INSTALL_PACKAGE_UPDATES"/>
+        <permission name="android.permission.KILL_ALL_BACKGROUND_PROCESSES"/>
         <!-- Needed for test only -->
         <permission name="android.permission.ACCESS_MTP"/>
         <!-- Needed for test only -->
diff --git a/errorprone/refaster/EfficientXml.java b/errorprone/refaster/EfficientXml.java
index ae797c4..87a902a 100644
--- a/errorprone/refaster/EfficientXml.java
+++ b/errorprone/refaster/EfficientXml.java
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
 
diff --git a/errorprone/refaster/EfficientXml.java.refaster b/errorprone/refaster/EfficientXml.java.refaster
index 750c2db..c285e9b 100644
--- a/errorprone/refaster/EfficientXml.java.refaster
+++ b/errorprone/refaster/EfficientXml.java.refaster
Binary files differ
diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java
index 1c41d06..9940ca3 100644
--- a/graphics/java/android/graphics/BLASTBufferQueue.java
+++ b/graphics/java/android/graphics/BLASTBufferQueue.java
@@ -47,7 +47,7 @@
             TransactionHangCallback callback);
 
     public interface TransactionHangCallback {
-        void onTransactionHang(boolean isGpuHang);
+        void onTransactionHang(String reason);
     }
 
     /** Create a new connection with the surface flinger. */
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 00be5a6e..77284c41 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -109,6 +109,12 @@
         return (mSplitRule instanceof SplitPlaceholderRule);
     }
 
+    @NonNull
+    SplitInfo toSplitInfo() {
+        return new SplitInfo(mPrimaryContainer.toActivityStack(),
+                mSecondaryContainer.toActivityStack(), mSplitAttributes);
+    }
+
     static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
         final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
         final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index bf7326a..1d513e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1422,6 +1422,11 @@
     @GuardedBy("mLock")
     void updateContainer(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
+        if (!container.getTaskContainer().isVisible()) {
+            // Wait until the Task is visible to avoid unnecessary update when the Task is still in
+            // background.
+            return;
+        }
         if (launchPlaceholderIfNecessary(wct, container)) {
             // Placeholder was launched, the positions will be updated when the activity is added
             // to the secondary container.
@@ -1643,16 +1648,14 @@
     /**
      * Notifies listeners about changes to split states if necessary.
      */
+    @VisibleForTesting
     @GuardedBy("mLock")
-    private void updateCallbackIfNecessary() {
-        if (mEmbeddingCallback == null) {
+    void updateCallbackIfNecessary() {
+        if (mEmbeddingCallback == null || !readyToReportToClient()) {
             return;
         }
-        if (!allActivitiesCreated()) {
-            return;
-        }
-        List<SplitInfo> currentSplitStates = getActiveSplitStates();
-        if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) {
+        final List<SplitInfo> currentSplitStates = getActiveSplitStates();
+        if (mLastReportedSplitStates.equals(currentSplitStates)) {
             return;
         }
         mLastReportedSplitStates.clear();
@@ -1661,48 +1664,27 @@
     }
 
     /**
-     * @return a list of descriptors for currently active split states. If the value returned is
-     * null, that indicates that the active split states are in an intermediate state and should
-     * not be reported.
+     * Returns a list of descriptors for currently active split states.
      */
     @GuardedBy("mLock")
-    @Nullable
+    @NonNull
     private List<SplitInfo> getActiveSplitStates() {
-        List<SplitInfo> splitStates = new ArrayList<>();
+        final List<SplitInfo> splitStates = new ArrayList<>();
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i)
-                    .mSplitContainers;
-            for (SplitContainer container : splitContainers) {
-                if (container.getPrimaryContainer().isEmpty()
-                        || container.getSecondaryContainer().isEmpty()) {
-                    // We are in an intermediate state because either the split container is about
-                    // to be removed or the primary or secondary container are about to receive an
-                    // activity.
-                    return null;
-                }
-                final ActivityStack primaryContainer = container.getPrimaryContainer()
-                        .toActivityStack();
-                final ActivityStack secondaryContainer = container.getSecondaryContainer()
-                        .toActivityStack();
-                final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer,
-                        container.getSplitAttributes());
-                splitStates.add(splitState);
-            }
+            mTaskContainers.valueAt(i).getSplitStates(splitStates);
         }
         return splitStates;
     }
 
     /**
-     * Checks if all activities that are registered with the containers have already appeared in
-     * the client.
+     * Whether we can now report the split states to the client.
      */
-    private boolean allActivitiesCreated() {
+    @GuardedBy("mLock")
+    private boolean readyToReportToClient() {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
-            for (TaskFragmentContainer container : containers) {
-                if (!container.taskInfoActivityCountMatchesCreated()) {
-                    return false;
-                }
+            if (mTaskContainers.valueAt(i).isInIntermediateState()) {
+                // If any Task is in an intermediate state, wait for the server update.
+                return false;
             }
         }
         return true;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 00943f2d..231da05 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -221,6 +221,24 @@
         return mContainers.indexOf(child);
     }
 
+    /** Whether the Task is in an intermediate state waiting for the server update.*/
+    boolean isInIntermediateState() {
+        for (TaskFragmentContainer container : mContainers) {
+            if (container.isInIntermediateState()) {
+                // We are in an intermediate state to wait for server update on this TaskFragment.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
+    void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
+        for (SplitContainer container : mSplitContainers) {
+            outSplitStates.add(container.toSplitInfo());
+        }
+    }
+
     /**
      * A wrapper class which contains the display ID and {@link Configuration} of a
      * {@link TaskContainer}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 18712ae..71b8840 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -166,16 +166,34 @@
         return allActivities;
     }
 
-    /**
-     * Checks if the count of activities from the same process in task fragment info corresponds to
-     * the ones created and available on the client side.
-     */
-    boolean taskInfoActivityCountMatchesCreated() {
+    /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
+    boolean isInIntermediateState() {
         if (mInfo == null) {
-            return false;
+            // Haven't received onTaskFragmentAppeared event.
+            return true;
         }
-        return mPendingAppearedActivities.isEmpty()
-                && mInfo.getActivities().size() == collectNonFinishingActivities().size();
+        if (mInfo.isEmpty()) {
+            // Empty TaskFragment will be removed or will have activity launched into it soon.
+            return true;
+        }
+        if (!mPendingAppearedActivities.isEmpty()) {
+            // Reparented activity hasn't appeared.
+            return true;
+        }
+        // Check if there is any reported activity that is no longer alive.
+        for (IBinder token : mInfo.getActivities()) {
+            final Activity activity = mController.getActivity(token);
+            if (activity == null && !mTaskContainer.isVisible()) {
+                // Activity can be null if the activity is not attached to process yet. That can
+                // happen when the activity is started in background.
+                continue;
+            }
+            if (activity == null || activity.isFinishing()) {
+                // One of the reported activity is no longer alive, wait for the server update.
+                return true;
+            }
+        }
+        return false;
     }
 
     @NonNull
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index a403031..87d0278 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -102,6 +102,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Test class for {@link SplitController}.
@@ -132,6 +133,8 @@
 
     private SplitController mSplitController;
     private SplitPresenter mSplitPresenter;
+    private Consumer<List<SplitInfo>> mEmbeddingCallback;
+    private List<SplitInfo> mSplitInfos;
     private TransactionManager mTransactionManager;
 
     @Before
@@ -141,9 +144,16 @@
                 .getCurrentWindowLayoutInfo(anyInt(), any());
         mSplitController = new SplitController(mWindowLayoutComponent);
         mSplitPresenter = mSplitController.mPresenter;
+        mSplitInfos = new ArrayList<>();
+        mEmbeddingCallback = splitInfos -> {
+            mSplitInfos.clear();
+            mSplitInfos.addAll(splitInfos);
+        };
+        mSplitController.setSplitInfoCallback(mEmbeddingCallback);
         mTransactionManager = mSplitController.mTransactionManager;
         spyOn(mSplitController);
         spyOn(mSplitPresenter);
+        spyOn(mEmbeddingCallback);
         spyOn(mTransactionManager);
         doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
         final Configuration activityConfig = new Configuration();
@@ -329,6 +339,30 @@
     }
 
     @Test
+    public void testUpdateContainer_skipIfTaskIsInvisible() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+        final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+        final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0);
+        spyOn(taskContainer);
+
+        // No update when the Task is invisible.
+        clearInvocations(mSplitPresenter);
+        doReturn(false).when(taskContainer).isVisible();
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+        verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+
+        // Update the split when the Task is visible.
+        doReturn(true).when(taskContainer).isVisible();
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+        verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
+                taskFragmentContainer, mTransaction);
+    }
+
+    @Test
     public void testOnStartActivityResultError() {
         final Intent intent = new Intent();
         final TaskContainer taskContainer = createTestTaskContainer();
@@ -1162,14 +1196,69 @@
                         new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
     }
 
+    @Test
+    public void testSplitInfoCallback_reportSplit() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+
+        mSplitController.updateCallbackIfNecessary();
+        assertEquals(1, mSplitInfos.size());
+        final SplitInfo splitInfo = mSplitInfos.get(0);
+        assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size());
+        assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size());
+        assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0));
+        assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0));
+    }
+
+    @Test
+    public void testSplitInfoCallback_reportSplitInMultipleTasks() {
+        final int taskId0 = 1;
+        final int taskId1 = 2;
+        final Activity r0 = createMockActivity(taskId0);
+        final Activity r1 = createMockActivity(taskId0);
+        final Activity r2 = createMockActivity(taskId1);
+        final Activity r3 = createMockActivity(taskId1);
+        addSplitTaskFragments(r0, r1);
+        addSplitTaskFragments(r2, r3);
+
+        mSplitController.updateCallbackIfNecessary();
+        assertEquals(2, mSplitInfos.size());
+    }
+
+    @Test
+    public void testSplitInfoCallback_doNotReportIfInIntermediateState() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+        final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0);
+        final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1);
+        spyOn(tf0);
+        spyOn(tf1);
+
+        // Do not report if activity has not appeared in the TaskFragmentContainer in split.
+        doReturn(true).when(tf0).isInIntermediateState();
+        mSplitController.updateCallbackIfNecessary();
+        verify(mEmbeddingCallback, never()).accept(any());
+
+        doReturn(false).when(tf0).isInIntermediateState();
+        mSplitController.updateCallbackIfNecessary();
+        verify(mEmbeddingCallback).accept(any());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
+        return createMockActivity(TASK_ID);
+    }
+
+    /** Creates a mock activity in the organizer process. */
+    private Activity createMockActivity(int taskId) {
         final Activity activity = mock(Activity.class);
         doReturn(mActivityResources).when(activity).getResources();
         final IBinder activityToken = new Binder();
         doReturn(activityToken).when(activity).getActivityToken();
         doReturn(activity).when(mSplitController).getActivity(activityToken);
-        doReturn(TASK_ID).when(activity).getTaskId();
+        doReturn(taskId).when(activity).getTaskId();
         doReturn(new ActivityInfo()).when(activity).getActivityInfo();
         doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
         return activity;
@@ -1177,7 +1266,8 @@
 
     /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
     private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
-        final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID);
+        final TaskFragmentContainer container = mSplitController.newContainer(activity,
+                activity.getTaskId());
         setupTaskFragmentInfo(container, activity);
         return container;
     }
@@ -1268,7 +1358,7 @@
 
         // We need to set those in case we are not respecting clear top.
         // TODO(b/231845476) we should always respect clearTop.
-        final int windowingMode = mSplitController.getTaskContainer(TASK_ID)
+        final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId())
                 .getWindowingModeForSplitTaskFragment(TASK_BOUNDS);
         primaryContainer.setLastRequestedWindowingMode(windowingMode);
         secondaryContainer.setLastRequestedWindowingMode(windowingMode);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 35415d8..d43c471 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -334,6 +334,70 @@
         assertFalse(container.hasActivity(mActivity.getActivityToken()));
     }
 
+    @Test
+    public void testIsInIntermediateState() {
+        // True if no info set.
+        final TaskContainer taskContainer = createTestTaskContainer();
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                mIntent, taskContainer, mController);
+        spyOn(taskContainer);
+        doReturn(true).when(taskContainer).isVisible();
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // True if empty info set.
+        final List<IBinder> activities = new ArrayList<>();
+        doReturn(activities).when(mInfo).getActivities();
+        doReturn(true).when(mInfo).isEmpty();
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if info is not empty.
+        doReturn(false).when(mInfo).isEmpty();
+        container.setInfo(mTransaction, mInfo);
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+
+        // True if there is pending appeared activity.
+        container.addPendingAppearedActivity(mActivity);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // True if the activity is finishing.
+        activities.add(mActivity.getActivityToken());
+        doReturn(true).when(mActivity).isFinishing();
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if the activity is not finishing.
+        doReturn(false).when(mActivity).isFinishing();
+        container.setInfo(mTransaction, mInfo);
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+
+        // True if there is a token that can't find associated activity.
+        activities.clear();
+        activities.add(new Binder());
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if there is a token that can't find associated activity when the Task is invisible.
+        doReturn(false).when(taskContainer).isVisible();
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
         final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml
index 57a7ad0..405cb06 100644
--- a/libs/WindowManager/Shell/res/values-am/strings.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"አስፋ"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"አሳንስ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"ዝጋ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"ተመለስ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"መያዣ"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml
index 23f1c6f..9321d8d 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"تكبير"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"تصغير"</string>
     <string name="close_button_text" msgid="2913281996024033299">"إغلاق"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"رجوع"</string>
+    <string name="handle_text" msgid="1766582106752184456">"مقبض"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml
index 57a763e..268827c 100644
--- a/libs/WindowManager/Shell/res/values-as/strings.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"সৰ্বাধিক মাত্ৰালৈ বঢ়াওক"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"মিনিমাইজ কৰক"</string>
     <string name="close_button_text" msgid="2913281996024033299">"বন্ধ কৰক"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"উভতি যাওক"</string>
+    <string name="handle_text" msgid="1766582106752184456">"হেণ্ডেল"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml
index 610ee10..779bfb6 100644
--- a/libs/WindowManager/Shell/res/values-az/strings.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Böyüdün"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Kiçildin"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Bağlayın"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Geriyə"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Hər kəsə açıq istifadəçi adı"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml
index 1a24478..5524c19 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Разгарнуць"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Згарнуць"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Закрыць"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml
index 1269c37..42dbe3e 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Увеличаване"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Намаляване"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Затваряне"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Манипулатор"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml
index 71c805f..1d674a5 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -86,6 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiziranje"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimiziranje"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Zatvaranje"</string>
-    <string name="back_button_text" msgid="1469718707134137085">"Natrag"</string>
-    <string name="handle_text" msgid="1766582106752184456">"Pokazivač"</string>
+    <string name="back_button_text" msgid="1469718707134137085">"Nazad"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml
index 564d448..3346938 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximitza"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimitza"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Tanca"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Enrere"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ansa"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml
index 4729c23..829bb91 100644
--- a/libs/WindowManager/Shell/res/values-da/strings.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimér"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Luk"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Tilbage"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Håndtag"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml
index 969eef8..b3bd9ea 100644
--- a/libs/WindowManager/Shell/res/values-de/strings.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximieren"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimieren"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Schließen"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Zurück"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ziehpunkt"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
index 7965358..1e7174d 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Controlador"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index d39fd41..87cdca4 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Cerrar"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Controlador"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml
index cb26c0a..f392560 100644
--- a/libs/WindowManager/Shell/res/values-et/strings.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimeeri"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimeeri"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Sule"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Tagasi"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Käepide"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 1dd88d9..9949dd2 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"بزرگ کردن"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"کوچک کردن"</string>
     <string name="close_button_text" msgid="2913281996024033299">"بستن"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"برگشتن"</string>
+    <string name="handle_text" msgid="1766582106752184456">"دستگیره"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml
index b6224ef..e701452 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Suurenna"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Pienennä"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Sulje"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Takaisin"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Kahva"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 4f992f5..8ceeec0 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Agrandir"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Réduire"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Fermer"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Retour"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Poignée"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml
index b349302..999921a 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximizar"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizar"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Pechar"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Atrás"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Controlador"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index c2732ec..1b9b90b 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"बड़ा करें"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"विंडो छोटी करें"</string>
     <string name="close_button_text" msgid="2913281996024033299">"बंद करें"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"वापस जाएं"</string>
+    <string name="handle_text" msgid="1766582106752184456">"हैंडल"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml
index ca98d6b..b61ea1d 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Ծավալել"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Ծալել"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Փակել"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Հետ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Նշիչ"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml
index b3bbba1..79e926d 100644
--- a/libs/WindowManager/Shell/res/values-in/strings.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimalkan"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimalkan"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Tutup"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Kembali"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Tuas"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml
index 456f152..0645c41 100644
--- a/libs/WindowManager/Shell/res/values-is/strings.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Stækka"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minnka"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Loka"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Til baka"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Handfang"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml
index 2f8b774..57238ea 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"הגדלה"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string>
     <string name="close_button_text" msgid="2913281996024033299">"סגירה"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"חזרה"</string>
+    <string name="handle_text" msgid="1766582106752184456">"נקודת אחיזה"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml
index e15b376..b0cf539 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"მაქსიმალურად გაშლა"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ჩაკეცვა"</string>
     <string name="close_button_text" msgid="2913281996024033299">"დახურვა"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"უკან"</string>
+    <string name="handle_text" msgid="1766582106752184456">"იდენტიფიკატორი"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml
index a8fd31d..a9f350e 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Жаю"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Кішірейту"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Жабу"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Артқа"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Идентификатор"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml
index bdfd775..a1d2691 100644
--- a/libs/WindowManager/Shell/res/values-km/strings.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ពង្រីក"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"បង្រួម"</string>
     <string name="close_button_text" msgid="2913281996024033299">"បិទ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"ថយក្រោយ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ឈ្មោះអ្នកប្រើប្រាស់"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml
index bb52084..f6ea6cc 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"최대화"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"최소화"</string>
     <string name="close_button_text" msgid="2913281996024033299">"닫기"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"뒤로"</string>
+    <string name="handle_text" msgid="1766582106752184456">"핸들"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml
index d5e3d84..53d4f34 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ຫຍໍ້ລົງ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"ປິດ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"ກັບຄືນ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ມືບັງຄັບ"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml
index db2c717..331281a 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Padidinti"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Sumažinti"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Uždaryti"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Atgal"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Rankenėlė"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml
index 6b1f76c..d301721 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizēt"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizēt"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Aizvērt"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Atpakaļ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Turis"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml
index ab3286d..9722868 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"വലുതാക്കുക"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ചെറുതാക്കുക"</string>
     <string name="close_button_text" msgid="2913281996024033299">"അടയ്ക്കുക"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"മടങ്ങുക"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ഹാൻഡിൽ"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml
index 678a2c5..29f57fb 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"मोठे करा"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"लहान करा"</string>
     <string name="close_button_text" msgid="2913281996024033299">"बंद करा"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"मागे जा"</string>
+    <string name="handle_text" msgid="1766582106752184456">"हँडल"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml
index 4620012..f4f3af8 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimer"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimer"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Lukk"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Tilbake"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Håndtak"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml
index cdddcdc..4b90e92 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string>
     <string name="close_button_text" msgid="2913281996024033299">"बन्द गर्नुहोस्"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"पछाडि"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ह्यान्डल"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml
index d31d7e4..18a2021 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maximaliseren"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimaliseren"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Sluiten"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Terug"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Gebruikersnaam"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml
index 48c9a9f..5c255d8 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"ਵੱਡਾ ਕਰੋ"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"ਛੋਟਾ ਕਰੋ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"ਬੰਦ ਕਰੋ"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"ਪਿੱਛੇ"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ਹੈਂਡਲ"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml
index 347b01d..086726c 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksymalizuj"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimalizuj"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Zamknij"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Wstecz"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Uchwyt"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml
index 3b6efc1..97c5a06 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Развернуть"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Свернуть"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Закрыть"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml
index 4be32cf..60e1222 100644
--- a/libs/WindowManager/Shell/res/values-si/strings.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"විහිදන්න"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"කුඩා කරන්න"</string>
     <string name="close_button_text" msgid="2913281996024033299">"වසන්න"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"ආපසු"</string>
+    <string name="handle_text" msgid="1766582106752184456">"හැඬලය"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml
index e4fa7e9..6dbd883 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimiraj"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimiraj"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Zapri"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Nazaj"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ročica"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml
index bbd312b..11e5713 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Maksimizo"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimizo"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Mbyll"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Pas"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Emërtimi"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml
index c4bcef4..c175583 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Utöka"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Minimera"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Stäng"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Tillbaka"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Handtag"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml
index 5ad1985..a049364 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Panua"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Punguza"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Funga"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Rudi nyuma"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Ncha"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml
index 1cb9cd76..21b7412 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"பெரிதாக்கும்"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"சிறிதாக்கும்"</string>
     <string name="close_button_text" msgid="2913281996024033299">"மூடும்"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"பின்செல்லும்"</string>
+    <string name="handle_text" msgid="1766582106752184456">"ஹேண்டில்"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml
index 7c557cb..dca58b8 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Ekranı Kapla"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Küçült"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Kapat"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Geri"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Herkese açık kullanıcı adı"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml
index 73cb754..f7a59d3 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Збільшити"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Згорнути"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Закрити"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Назад"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Маркер"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml
index 1cf6228..ee9b4dc 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Yoyish"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Kichraytirish"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Yopish"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Orqaga"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Identifikator"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml
index ce10e46..67ab2a0 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"Phóng to"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"Thu nhỏ"</string>
     <string name="close_button_text" msgid="2913281996024033299">"Đóng"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"Quay lại"</string>
+    <string name="handle_text" msgid="1766582106752184456">"Xử lý"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
index 824f46e..8fdf1d4 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
@@ -86,8 +86,6 @@
     <string name="maximize_button_text" msgid="1650859196290301963">"最大化"</string>
     <string name="minimize_button_text" msgid="271592547935841753">"最小化"</string>
     <string name="close_button_text" msgid="2913281996024033299">"关闭"</string>
-    <!-- no translation found for back_button_text (1469718707134137085) -->
-    <skip />
-    <!-- no translation found for handle_text (1766582106752184456) -->
-    <skip />
+    <string name="back_button_text" msgid="1469718707134137085">"返回"</string>
+    <string name="handle_text" msgid="1766582106752184456">"处理"</string>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml
index d452d25..b470e7f 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings.xml
@@ -19,7 +19,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string>
     <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string>
-    <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string>
+    <string name="pip_phone_settings" msgid="5468987116750491918">"Amasethingi"</string>
     <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string>
     <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string>
     <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"Imenyu Yesithombe-Esithombeni"</string>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 43f39b7..dc5b9a1e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -72,14 +72,15 @@
     private static final String TAG = "BackAnimationController";
     private static final int SETTING_VALUE_OFF = 0;
     private static final int SETTING_VALUE_ON = 1;
-    private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
-            "persist.wm.debug.predictive_back_progress_threshold";
     public static final boolean IS_ENABLED =
             SystemProperties.getInt("persist.wm.debug.predictive_back",
-                    SETTING_VALUE_ON) != SETTING_VALUE_OFF;
-    private static final int PROGRESS_THRESHOLD = SystemProperties
-            .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
-
+                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
+     /** Flag for U animation features */
+    public static boolean IS_U_ANIMATION_ENABLED =
+            SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
+                    SETTING_VALUE_OFF) == SETTING_VALUE_ON;
+    /** Predictive back animation developer option */
+    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
     // TODO (b/241808055) Find a appropriate time to remove during refactor
     private static final boolean ENABLE_SHELL_TRANSITIONS = Transitions.ENABLE_SHELL_TRANSITIONS;
     /**
@@ -88,8 +89,6 @@
      */
     private static final long MAX_TRANSITION_DURATION = 2000;
 
-    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
-
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
 
@@ -143,53 +142,6 @@
         }
     };
 
-    /**
-     * Helper class to record the touch location for gesture start and latest.
-     */
-    private static class TouchTracker {
-        /**
-         * Location of the latest touch event
-         */
-        private float mLatestTouchX;
-        private float mLatestTouchY;
-        private int mSwipeEdge;
-        private float mProgressThreshold;
-
-        /**
-         * Location of the initial touch event of the back gesture.
-         */
-        private float mInitTouchX;
-        private float mInitTouchY;
-
-        void update(float touchX, float touchY, int swipeEdge) {
-            mLatestTouchX = touchX;
-            mLatestTouchY = touchY;
-            mSwipeEdge = swipeEdge;
-        }
-
-        void setGestureStartLocation(float touchX, float touchY) {
-            mInitTouchX = touchX;
-            mInitTouchY = touchY;
-        }
-
-        void setProgressThreshold(float progressThreshold) {
-            mProgressThreshold = progressThreshold;
-        }
-
-        float getProgress(float touchX) {
-            int deltaX = Math.round(touchX - mInitTouchX);
-            float progressThreshold = PROGRESS_THRESHOLD >= 0
-                    ? PROGRESS_THRESHOLD : mProgressThreshold;
-            return Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1);
-        }
-
-        void reset() {
-            mInitTouchX = 0;
-            mInitTouchY = 0;
-            mSwipeEdge = -1;
-        }
-    }
-
     public BackAnimationController(
             @NonNull ShellInit shellInit,
             @NonNull ShellController shellController,
@@ -221,6 +173,11 @@
         mTransitions = transitions;
     }
 
+    @VisibleForTesting
+    void setEnableUAnimation(boolean enable) {
+        IS_U_ANIMATION_ENABLED = enable;
+    }
+
     private void onInit() {
         setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
         createAdapter();
@@ -374,7 +331,8 @@
         if (mTransitionInProgress) {
             return;
         }
-        mTouchTracker.update(touchX, touchY, swipeEdge);
+
+        mTouchTracker.update(touchX, touchY);
         if (keyAction == MotionEvent.ACTION_DOWN) {
             if (!mBackGestureStarted) {
                 mShouldStartOnNextMoveEvent = true;
@@ -384,7 +342,7 @@
                 // Let the animation initialized here to make sure the onPointerDownOutsideFocus
                 // could be happened when ACTION_DOWN, it may change the current focus that we
                 // would access it when startBackNavigation.
-                onGestureStarted(touchX, touchY);
+                onGestureStarted(touchX, touchY, swipeEdge);
                 mShouldStartOnNextMoveEvent = false;
             }
             onMove(touchX, touchY, swipeEdge);
@@ -398,14 +356,14 @@
         }
     }
 
-    private void onGestureStarted(float touchX, float touchY) {
+    private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
         if (mBackGestureStarted || mBackNavigationInfo != null) {
             Log.e(TAG, "Animation is being initialized but is already started.");
             finishBackNavigation();
         }
 
-        mTouchTracker.setGestureStartLocation(touchX, touchY);
+        mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
         mBackGestureStarted = true;
 
         try {
@@ -428,12 +386,10 @@
         final IOnBackInvokedCallback targetCallback;
         final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(backType);
         if (shouldDispatchToAnimator) {
-            targetCallback = mAnimationDefinition.get(backType).getGestureStartedCallback();
+            mAnimationDefinition.get(backType).startGesture();
         } else {
             targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
-        }
-        if (shouldDispatchToAnimator) {
-            dispatchOnBackStarted(targetCallback);
+            dispatchOnBackStarted(targetCallback, mTouchTracker.createStartEvent(null));
         }
     }
 
@@ -441,12 +397,10 @@
         if (!mBackGestureStarted || mBackNavigationInfo == null || !mEnableAnimations.get()) {
             return;
         }
-        mTouchTracker.update(touchX, touchY, swipeEdge);
-        float progress = mTouchTracker.getProgress(touchX);
-        int backType = mBackNavigationInfo.getType();
+        final BackEvent backEvent = mTouchTracker.createProgressEvent();
 
-        BackEvent backEvent = new BackEvent(touchX, touchY, progress, swipeEdge);
-        IOnBackInvokedCallback targetCallback = null;
+        int backType = mBackNavigationInfo.getType();
+        IOnBackInvokedCallback targetCallback;
         if (shouldDispatchToAnimator(backType)) {
             targetCallback = mAnimationDefinition.get(backType).getCallback();
         } else {
@@ -532,18 +486,21 @@
                 && mAnimationDefinition.contains(backType);
     }
 
-    private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) {
+    private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
+            BackEvent backEvent) {
         if (callback == null) {
             return;
         }
         try {
-            callback.onBackStarted();
+            if (shouldDispatchAnimation(callback)) {
+                callback.onBackStarted(backEvent);
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackStarted error: ", e);
         }
     }
 
-    private static void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
+    private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
         if (callback == null) {
             return;
         }
@@ -554,29 +511,39 @@
         }
     }
 
-    private static void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
+    private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
         if (callback == null) {
             return;
         }
         try {
-            callback.onBackCancelled();
+            if (shouldDispatchAnimation(callback)) {
+                callback.onBackCancelled();
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackCancelled error: ", e);
         }
     }
 
-    private static void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
+    private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
             BackEvent backEvent) {
         if (callback == null) {
             return;
         }
         try {
-            callback.onBackProgressed(backEvent);
+            if (shouldDispatchAnimation(callback)) {
+                callback.onBackProgressed(backEvent);
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "dispatchOnBackProgressed error: ", e);
         }
     }
 
+    private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
+        return (IS_U_ANIMATION_ENABLED || callback == mAnimationDefinition.get(
+                BackNavigationInfo.TYPE_RETURN_TO_HOME).getCallback())
+                && mEnableAnimations.get();
+    }
+
     /**
      * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
      */
@@ -585,6 +552,7 @@
             return;
         }
         mTriggerBack = triggerBack;
+        mTouchTracker.setTriggerBack(triggerBack);
     }
 
     private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
@@ -670,13 +638,18 @@
                     ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
                     runner.startAnimation(apps, wallpapers, nonApps,
                             BackAnimationController.this::onBackAnimationFinished);
+                    if (apps.length >= 1) {
+                        final int backType = mBackNavigationInfo.getType();
+                        IOnBackInvokedCallback targetCallback = mAnimationDefinition.get(backType)
+                                .getCallback();
+                        dispatchOnBackStarted(
+                                targetCallback, mTouchTracker.createStartEvent(apps[0]));
+                    }
 
                     if (!mBackGestureStarted) {
                         // if the down -> up gesture happened before animation start, we have to
                         // trigger the uninterruptible transition to finish the back animation.
-                        final BackEvent backFinish = new BackEvent(
-                                mTouchTracker.mLatestTouchX, mTouchTracker.mLatestTouchY, 1,
-                                mTouchTracker.mSwipeEdge);
+                        final BackEvent backFinish = mTouchTracker.createProgressEvent(1);
                         startTransition();
                         runner.consumeIfGestureFinished(backFinish);
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index 12bbf73..c53fcfc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -79,9 +79,8 @@
         }
     }
 
-    IOnBackInvokedCallback getGestureStartedCallback() {
+    void startGesture() {
         mWaitingAnimation = true;
-        return mCallback;
     }
 
     boolean onGestureFinished(boolean triggerBack) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
new file mode 100644
index 0000000..ccfac65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.back;
+
+import android.os.SystemProperties;
+import android.view.RemoteAnimationTarget;
+import android.window.BackEvent;
+
+/**
+ * Helper class to record the touch location for gesture and generate back events.
+ */
+class TouchTracker {
+    private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
+            "persist.wm.debug.predictive_back_progress_threshold";
+    private static final int PROGRESS_THRESHOLD = SystemProperties
+            .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
+    private float mProgressThreshold;
+    /**
+     * Location of the latest touch event
+     */
+    private float mLatestTouchX;
+    private float mLatestTouchY;
+    private boolean mTriggerBack;
+
+    /**
+     * Location of the initial touch event of the back gesture.
+     */
+    private float mInitTouchX;
+    private float mInitTouchY;
+    private float mStartThresholdX;
+    private int mSwipeEdge;
+    private boolean mCancelled;
+
+    void update(float touchX, float touchY) {
+        /**
+         * If back was previously cancelled but the user has started swiping in the forward
+         * direction again, restart back.
+         */
+        if (mCancelled && ((touchX > mLatestTouchX && mSwipeEdge == BackEvent.EDGE_LEFT)
+                || touchX < mLatestTouchX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
+            mCancelled = false;
+            mStartThresholdX = touchX;
+        }
+        mLatestTouchX = touchX;
+        mLatestTouchY = touchY;
+    }
+
+    void setTriggerBack(boolean triggerBack) {
+        if (mTriggerBack != triggerBack && !triggerBack) {
+            mCancelled = true;
+        }
+        mTriggerBack = triggerBack;
+    }
+
+    void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
+        mInitTouchX = touchX;
+        mInitTouchY = touchY;
+        mSwipeEdge = swipeEdge;
+        mStartThresholdX = mInitTouchX;
+    }
+
+    void reset() {
+        mInitTouchX = 0;
+        mInitTouchY = 0;
+        mStartThresholdX = 0;
+        mCancelled = false;
+        mTriggerBack = false;
+        mSwipeEdge = BackEvent.EDGE_LEFT;
+    }
+
+    BackEvent createStartEvent(RemoteAnimationTarget target) {
+        return new BackEvent(mInitTouchX, mInitTouchY, 0, mSwipeEdge, target);
+    }
+
+    BackEvent createProgressEvent() {
+        float progressThreshold = PROGRESS_THRESHOLD >= 0
+                ? PROGRESS_THRESHOLD : mProgressThreshold;
+        progressThreshold = progressThreshold == 0 ? 1 : progressThreshold;
+        float progress = 0;
+        // Progress is always 0 when back is cancelled and not restarted.
+        if (!mCancelled) {
+            // If back is committed, progress is the distance between the last and first touch
+            // point, divided by the max drag distance. Otherwise, it's the distance between
+            // the last touch point and the starting threshold, divided by max drag distance.
+            // The starting threshold is initially the first touch location, and updated to
+            // the location everytime back is restarted after being cancelled.
+            float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
+            float deltaX = Math.max(
+                    mSwipeEdge == BackEvent.EDGE_LEFT
+                            ? mLatestTouchX - startX
+                            : startX - mLatestTouchX,
+                    0);
+            progress = Math.min(Math.max(deltaX / progressThreshold, 0), 1);
+        }
+        return createProgressEvent(progress);
+    }
+
+    BackEvent createProgressEvent(float progress) {
+        return new BackEvent(mLatestTouchX, mLatestTouchY, progress, mSwipeEdge, null);
+    }
+
+    public void setProgressThreshold(float progressThreshold) {
+        mProgressThreshold = progressThreshold;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
index d6803e8..d3a9a67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleBadgeIconFactory.java
@@ -52,7 +52,7 @@
             userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon);
         }
         Bitmap userBadgedBitmap = createIconBitmap(
-                userBadgedAppIcon, 1, BITMAP_GENERATION_MODE_WITH_SHADOW);
+                userBadgedAppIcon, 1, MODE_WITH_SHADOW);
         return createIconBitmap(userBadgedBitmap);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
index 5dab8a0..4ded3ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleIconFactory.java
@@ -79,6 +79,6 @@
                 true /* shrinkNonAdaptiveIcons */,
                 null /* outscale */,
                 outScale);
-        return createIconBitmap(icon, outScale[0], BITMAP_GENERATION_MODE_WITH_SHADOW);
+        return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW);
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 266cf29..66202ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -21,12 +21,10 @@
 import android.animation.ValueAnimator;
 import android.annotation.IntDef;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.IDisplayWindowInsetsController;
@@ -43,7 +41,7 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.internal.view.IInputMethodManager;
+import com.android.internal.inputmethod.IInputMethodManagerGlobal;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
@@ -514,16 +512,10 @@
     }
 
     void removeImeSurface() {
-        final IInputMethodManager imms = getImms();
-        if (imms != null) {
-            try {
-                // Remove the IME surface to make the insets invisible for
-                // non-client controlled insets.
-                imms.removeImeSurface();
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Failed to remove IME surface.", e);
-            }
-        }
+        // Remove the IME surface to make the insets invisible for
+        // non-client controlled insets.
+        IInputMethodManagerGlobal.removeImeSurface(
+                e -> Slog.e(TAG, "Failed to remove IME surface.", e));
     }
 
     /**
@@ -597,11 +589,6 @@
         }
     }
 
-    public IInputMethodManager getImms() {
-        return IInputMethodManager.Stub.asInterface(
-                ServiceManager.getService(Context.INPUT_METHOD_SERVICE));
-    }
-
     private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) {
         if (a == b) {
             return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 44a467f..cbd544c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -18,9 +18,21 @@
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
+
 /**
  * Interface to interact with desktop mode feature in shell.
  */
 @ExternalThread
 public interface DesktopMode {
+
+    /**
+     * Adds a listener to find out about changes in the visibility of freeform tasks.
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+            Executor callbackExecutor);
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index b96facf..34ff6d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
@@ -60,6 +61,7 @@
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.concurrent.Executor;
 
 /**
  * Handles windowing changes when desktop mode system setting changes
@@ -132,6 +134,17 @@
         return new IDesktopModeImpl(this);
     }
 
+    /**
+     * Adds a listener to find out about changes in the visibility of freeform tasks.
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+            Executor callbackExecutor) {
+        mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
+    }
+
     @VisibleForTesting
     void updateDesktopModeActive(boolean active) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -181,7 +194,18 @@
     /**
      * Show apps on desktop
      */
-    WindowContainerTransaction showDesktopApps() {
+    void showDesktopApps() {
+        WindowContainerTransaction wct = bringDesktopAppsToFront();
+
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */);
+        } else {
+            mShellTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    @NonNull
+    private WindowContainerTransaction bringDesktopAppsToFront() {
         ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
         ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -197,11 +221,6 @@
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
-
-        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mShellTaskOrganizer.applyTransaction(wct);
-        }
-
         return wct;
     }
 
@@ -237,17 +256,29 @@
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @NonNull TransitionRequestInfo request) {
-
-        // Only do anything if we are in desktop mode and opening a task/app
-        if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) {
+        // Only do anything if we are in desktop mode and opening a task/app in freeform
+        if (!DesktopModeStatus.isActive(mContext)) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "skip shell transition request: desktop mode not active");
             return null;
         }
+        if (request.getType() != TRANSIT_OPEN) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "skip shell transition request: only supports TRANSIT_OPEN");
+            return null;
+        }
+        if (request.getTriggerTask() == null
+                || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task");
+            return null;
+        }
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request);
 
         WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
         if (wct == null) {
             wct = new WindowContainerTransaction();
         }
-        wct.merge(showDesktopApps(), true /* transfer */);
+        wct.merge(bringDesktopAppsToFront(), true /* transfer */);
         wct.reorder(request.getTriggerTask().token, true /* onTop */);
 
         return wct;
@@ -293,7 +324,14 @@
      */
     @ExternalThread
     private final class DesktopModeImpl implements DesktopMode {
-        // Do nothing
+
+        @Override
+        public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+                Executor callbackExecutor) {
+            mMainExecutor.execute(() -> {
+                DesktopModeController.this.addListener(listener, callbackExecutor);
+            });
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 988601c..c91d54a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -16,7 +16,9 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.util.ArrayMap
 import android.util.ArraySet
+import java.util.concurrent.Executor
 
 /**
  * Keeps track of task data related to desktop mode.
@@ -30,20 +32,39 @@
      * Task gets removed from this list when it vanishes. Or when desktop mode is turned off.
      */
     private val activeTasks = ArraySet<Int>()
-    private val listeners = ArraySet<Listener>()
+    private val visibleTasks = ArraySet<Int>()
+    private val activeTasksListeners = ArraySet<ActiveTasksListener>()
+    // Track visible tasks separately because a task may be part of the desktop but not visible.
+    private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
 
     /**
-     * Add a [Listener] to be notified of updates to the repository.
+     * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository.
      */
-    fun addListener(listener: Listener) {
-        listeners.add(listener)
+    fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
+        activeTasksListeners.add(activeTasksListener)
     }
 
     /**
-     * Remove a previously registered [Listener]
+     * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
      */
-    fun removeListener(listener: Listener) {
-        listeners.remove(listener)
+    fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
+        visibleTasksListeners.put(visibleTasksListener, executor)
+        executor.execute(
+                Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) })
+    }
+
+    /**
+     * Remove a previously registered [ActiveTasksListener]
+     */
+    fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
+        activeTasksListeners.remove(activeTasksListener)
+    }
+
+    /**
+     * Remove a previously registered [VisibleTasksListener]
+     */
+    fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
+        visibleTasksListeners.remove(visibleTasksListener)
     }
 
     /**
@@ -52,7 +73,7 @@
     fun addActiveTask(taskId: Int) {
         val added = activeTasks.add(taskId)
         if (added) {
-            listeners.onEach { it.onActiveTasksChanged() }
+            activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
     }
 
@@ -62,7 +83,7 @@
     fun removeActiveTask(taskId: Int) {
         val removed = activeTasks.remove(taskId)
         if (removed) {
-            listeners.onEach { it.onActiveTasksChanged() }
+            activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
     }
 
@@ -81,9 +102,43 @@
     }
 
     /**
-     * Defines interface for classes that can listen to changes in repository state.
+     * Updates whether a freeform task with this id is visible or not and notifies listeners.
      */
-    interface Listener {
-        fun onActiveTasksChanged()
+    fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
+        val prevCount: Int = visibleTasks.size
+        if (visible) {
+            visibleTasks.add(taskId)
+        } else {
+            visibleTasks.remove(taskId)
+        }
+        if (prevCount == 0 && visibleTasks.size == 1 ||
+                prevCount > 0 && visibleTasks.size == 0) {
+            for ((listener, executor) in visibleTasksListeners) {
+                executor.execute(
+                        Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) })
+            }
+        }
+    }
+
+    /**
+     * Defines interface for classes that can listen to changes for active tasks in desktop mode.
+     */
+    interface ActiveTasksListener {
+        /**
+         * Called when the active tasks change in desktop mode.
+         */
+        @JvmDefault
+        fun onActiveTasksChanged() {}
+    }
+
+    /**
+     * Defines interface for classes that can listen to changes for visible tasks in desktop mode.
+     */
+    interface VisibleTasksListener {
+        /**
+         * Called when the desktop starts or stops showing freeform tasks.
+         */
+        @JvmDefault
+        fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {}
     }
 }
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 f82a346..eaa7158 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
@@ -90,6 +90,8 @@
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "Adding active freeform task: #%d", taskInfo.taskId);
             mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
+            mDesktopModeTaskRepository.ifPresent(
+                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true));
         }
     }
 
@@ -103,6 +105,8 @@
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "Removing active freeform task: #%d", taskInfo.taskId);
             mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
+            mDesktopModeTaskRepository.ifPresent(
+                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false));
         }
 
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -124,6 +128,8 @@
                         "Adding active freeform task: #%d", taskInfo.taskId);
                 mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
             }
+            mDesktopModeTaskRepository.ifPresent(
+                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible));
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index afb64c9..43d3f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -60,7 +60,7 @@
         FloatingContentCoordinator.FloatingContent {
 
     public static final boolean ENABLE_FLING_TO_DISMISS_PIP =
-            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", true);
+            SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_pip", false);
     private static final String TAG = "PipMotionHelper";
     private static final boolean DEBUG = false;
 
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 08f3db6..f9172ba 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
@@ -68,7 +68,7 @@
  * Manages the recent task list from the system, caching it as necessary.
  */
 public class RecentTasksController implements TaskStackListenerCallback,
-        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener {
+        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
     private static final String TAG = RecentTasksController.class.getSimpleName();
 
     private final Context mContext;
@@ -147,7 +147,7 @@
                 this::createExternalInterface, this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
-        mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this));
+        mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 8cee4f1..6ce981e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -432,7 +432,8 @@
                     final ShapeIconFactory factory = new ShapeIconFactory(
                             SplashscreenContentDrawer.this.mContext,
                             scaledIconDpi, mFinalIconSize);
-                    final Bitmap bitmap = factory.createScaledBitmapWithoutShadow(iconDrawable);
+                    final Bitmap bitmap = factory.createScaledBitmap(iconDrawable,
+                            BaseIconFactory.MODE_DEFAULT);
                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                     createIconDrawable(new BitmapDrawable(bitmap), true,
                             mHighResIconProvider.mLoadInDetail);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 9c2c2fa..af79386 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -619,12 +619,13 @@
         // Animation length is already expected to be scaled.
         va.overrideDurationScale(1.0f);
         va.setDuration(anim.computeDurationHint());
-        va.addUpdateListener(animation -> {
+        final ValueAnimator.AnimatorUpdateListener updateListener = animation -> {
             final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
 
             applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
                     position, cornerRadius, clipRect);
-        });
+        };
+        va.addUpdateListener(updateListener);
 
         final Runnable finisher = () -> {
             applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
@@ -637,20 +638,30 @@
             });
         };
         va.addListener(new AnimatorListenerAdapter() {
+            // It is possible for the end/cancel to be called more than once, which may cause
+            // issues if the animating surface has already been released. Track the finished
+            // state here to skip duplicate callbacks. See b/252872225.
             private boolean mFinished = false;
 
             @Override
             public void onAnimationEnd(Animator animation) {
-                if (mFinished) return;
-                mFinished = true;
-                finisher.run();
+                onFinish();
             }
 
             @Override
             public void onAnimationCancel(Animator animation) {
+                onFinish();
+            }
+
+            private void onFinish() {
                 if (mFinished) return;
                 mFinished = true;
                 finisher.run();
+                // The update listener can continue to be called after the animation has ended if
+                // end() is called manually again before the finisher removes the animation.
+                // Remove it manually here to prevent animating a released surface.
+                // See b/252872225.
+                va.removeUpdateListener(updateListener);
             }
         });
         animations.add(va);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 2b27bae..66d0a2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.transition;
 
-import static android.hardware.HardwareBuffer.RGBA_8888;
-import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
 import static android.util.RotationUtils.deltaRotation;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
@@ -37,8 +35,6 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
 import android.util.Slog;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -50,12 +46,11 @@
 import android.window.TransitionInfo;
 
 import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
-import java.util.Arrays;
 
 /**
  * This class handles the rotation animation when the device is rotated.
@@ -173,7 +168,7 @@
                 t.setBuffer(mScreenshotLayer, hardwareBuffer);
                 t.show(mScreenshotLayer);
                 if (!isCustomRotate()) {
-                    mStartLuma = getMedianBorderLuma(hardwareBuffer, colorSpace);
+                    mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer, colorSpace);
                 }
             }
 
@@ -404,93 +399,6 @@
         mTransactionPool.release(t);
     }
 
-    /**
-     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
-     * luminance at the borders of the bitmap
-     * @return the average luminance of all the pixels at the borders of the bitmap
-     */
-    private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
-        // Cannot read content from buffer with protected usage.
-        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
-                || hasProtectedContent(hardwareBuffer)) {
-            return 0;
-        }
-
-        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
-                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
-        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
-        Image image = ir.acquireLatestImage();
-        if (image == null || image.getPlanes().length == 0) {
-            return 0;
-        }
-
-        Image.Plane plane = image.getPlanes()[0];
-        ByteBuffer buffer = plane.getBuffer();
-        int width = image.getWidth();
-        int height = image.getHeight();
-        int pixelStride = plane.getPixelStride();
-        int rowStride = plane.getRowStride();
-        float[] borderLumas = new float[2 * width + 2 * height];
-
-        // Grab the top and bottom borders
-        int l = 0;
-        for (int x = 0; x < width; x++) {
-            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
-        }
-
-        // Grab the left and right borders
-        for (int y = 0; y < height; y++) {
-            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
-        }
-
-        // Cleanup
-        ir.close();
-
-        // Oh, is this too simple and inefficient for you?
-        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
-        Arrays.sort(borderLumas);
-        return borderLumas[borderLumas.length / 2];
-    }
-
-    /**
-     * @return whether the hardwareBuffer passed in is marked as protected.
-     */
-    private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) {
-        return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
-    }
-
-    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
-            int pixelStride, int rowStride) {
-        int offset = y * rowStride + x * pixelStride;
-        int pixel = 0;
-        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
-        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
-        pixel |= (buffer.get(offset + 2) & 0xff);       // B
-        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
-        return Color.valueOf(pixel).luminance();
-    }
-
-    /**
-     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
-     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
-     */
-    private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) {
-        if (surfaceControl ==  null) {
-            return 0;
-        }
-
-        Rect crop = new Rect(0, 0, bounds.width(), bounds.height());
-        ScreenCapture.ScreenshotHardwareBuffer buffer =
-                ScreenCapture.captureLayers(surfaceControl, crop, 1);
-        if (buffer == null) {
-            return 0;
-        }
-
-        return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace());
-    }
-
     private static void applyColor(int startColor, int endColor, float[] rgbFloat,
             float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
         final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
index e903897..f209521 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/SplitBounds.java
@@ -33,6 +33,8 @@
     // This class is orientation-agnostic, so we compute both for later use
     public final float topTaskPercent;
     public final float leftTaskPercent;
+    public final float dividerWidthPercent;
+    public final float dividerHeightPercent;
     /**
      * If {@code true}, that means at the time of creation of this object, the
      * split-screened apps were vertically stacked. This is useful in scenarios like
@@ -62,8 +64,12 @@
             appsStackedVertically = false;
         }
 
-        leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right;
-        topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom;
+        float totalWidth = rightBottomBounds.right - leftTopBounds.left;
+        float totalHeight = rightBottomBounds.bottom - leftTopBounds.top;
+        leftTaskPercent = leftTopBounds.width() / totalWidth;
+        topTaskPercent = leftTopBounds.height() / totalHeight;
+        dividerWidthPercent = visualDividerBounds.width() / totalWidth;
+        dividerHeightPercent = visualDividerBounds.height() / totalHeight;
     }
 
     public SplitBounds(Parcel parcel) {
@@ -75,6 +81,8 @@
         appsStackedVertically = parcel.readBoolean();
         leftTopTaskId = parcel.readInt();
         rightBottomTaskId = parcel.readInt();
+        dividerWidthPercent = parcel.readInt();
+        dividerHeightPercent = parcel.readInt();
     }
 
     @Override
@@ -87,6 +95,8 @@
         parcel.writeBoolean(appsStackedVertically);
         parcel.writeInt(leftTopTaskId);
         parcel.writeInt(rightBottomTaskId);
+        parcel.writeFloat(dividerWidthPercent);
+        parcel.writeFloat(dividerHeightPercent);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 7d1f130..9d61c14 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -101,9 +101,9 @@
         final int shadowRadiusID = taskInfo.isFocused
                 ? R.dimen.freeform_decor_shadow_focused_thickness
                 : R.dimen.freeform_decor_shadow_unfocused_thickness;
-        final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
-                == WindowConfiguration.WINDOWING_MODE_FREEFORM;
-        final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
+        final boolean isFreeform =
+                taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+        final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
 
         WindowDecorLinearLayout oldRootView = mResult.mRootView;
         final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -114,6 +114,7 @@
         int outsetRightId = R.dimen.freeform_resize_handle;
         int outsetBottomId = R.dimen.freeform_resize_handle;
 
+        mRelayoutParams.reset();
         mRelayoutParams.mRunningTaskInfo = taskInfo;
         mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration;
         mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 01cab9a..b314163 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -91,7 +91,7 @@
     SurfaceControl mTaskBackgroundSurface;
 
     SurfaceControl mCaptionContainerSurface;
-    private CaptionWindowManager mCaptionWindowManager;
+    private WindowlessWindowManager mCaptionWindowManager;
     private SurfaceControlViewHost mViewHost;
 
     private final Rect mCaptionInsetsRect = new Rect();
@@ -199,13 +199,14 @@
         }
 
         final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
-        final int decorContainerOffsetX = -loadResource(params.mOutsetLeftId);
-        final int decorContainerOffsetY = -loadResource(params.mOutsetTopId);
+        final Resources resources = mDecorWindowContext.getResources();
+        final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
+        final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
         outResult.mWidth = taskBounds.width()
-                + loadResource(params.mOutsetRightId)
+                + loadDimensionPixelSize(resources, params.mOutsetRightId)
                 - decorContainerOffsetX;
         outResult.mHeight = taskBounds.height()
-                + loadResource(params.mOutsetBottomId)
+                + loadDimensionPixelSize(resources, params.mOutsetBottomId)
                 - decorContainerOffsetY;
         startT.setPosition(
                         mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
@@ -225,7 +226,7 @@
                     .build();
         }
 
-        float shadowRadius = loadResource(params.mShadowRadiusId);
+        float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
         int backgroundColorInt = mTaskInfo.taskDescription.getBackgroundColor();
         mTmpColor[0] = (float) Color.red(backgroundColorInt) / 255.f;
         mTmpColor[1] = (float) Color.green(backgroundColorInt) / 255.f;
@@ -248,8 +249,8 @@
                     .build();
         }
 
-        final int captionHeight = loadResource(params.mCaptionHeightId);
-        final int captionWidth = loadResource(params.mCaptionWidthId);
+        final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+        final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId);
 
         //Prevent caption from going offscreen if task is too high up
         final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2;
@@ -264,8 +265,9 @@
         if (mCaptionWindowManager == null) {
             // Put caption under a container surface because ViewRootImpl sets the destination frame
             // of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
-            mCaptionWindowManager = new CaptionWindowManager(
-                    mTaskInfo.getConfiguration(), mCaptionContainerSurface);
+            mCaptionWindowManager = new WindowlessWindowManager(
+                    mTaskInfo.getConfiguration(), mCaptionContainerSurface,
+                    null /* hostInputToken */);
         }
 
         // Caption view
@@ -309,13 +311,6 @@
                 .setCrop(mTaskSurface, mTaskSurfaceCrop);
     }
 
-    private int loadResource(int resourceId) {
-        if (resourceId == Resources.ID_NULL) {
-            return 0;
-        }
-        return mDecorWindowContext.getResources().getDimensionPixelSize(resourceId);
-    }
-
     /**
      * Obtains the {@link Display} instance for the display ID in {@link #mTaskInfo} if it exists or
      * registers {@link #mOnDisplaysChangedListener} if it doesn't.
@@ -374,33 +369,18 @@
         releaseViews();
     }
 
-    static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
-        int mWidth;
-        int mHeight;
-        T mRootView;
-
-        void reset() {
-            mWidth = 0;
-            mHeight = 0;
-            mRootView = null;
+    private static int loadDimensionPixelSize(Resources resources, int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
         }
+        return resources.getDimensionPixelSize(resourceId);
     }
 
-    private static class CaptionWindowManager extends WindowlessWindowManager {
-        CaptionWindowManager(Configuration config, SurfaceControl rootSurface) {
-            super(config, rootSurface, null /* hostInputToken */);
+    private static float loadDimension(Resources resources, int resourceId) {
+        if (resourceId == Resources.ID_NULL) {
+            return 0;
         }
-
-        @Override
-        public void setConfiguration(Configuration configuration) {
-            super.setConfiguration(configuration);
-        }
-    }
-
-    interface SurfaceControlViewHostFactory {
-        default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
-            return new SurfaceControlViewHost(c, d, wmm);
-        }
+        return resources.getDimension(resourceId);
     }
 
     static class RelayoutParams{
@@ -433,6 +413,23 @@
             mOutsetLeftId = Resources.ID_NULL;
             mOutsetRightId = Resources.ID_NULL;
         }
-
     }
-}
\ No newline at end of file
+
+    static class RelayoutResult<T extends View & TaskFocusStateConsumer> {
+        int mWidth;
+        int mHeight;
+        T mRootView;
+
+        void reset() {
+            mWidth = 0;
+            mHeight = 0;
+            mRootView = null;
+        }
+    }
+
+    interface SurfaceControlViewHostFactory {
+        default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
+            return new SurfaceControlViewHost(c, d, wmm);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index fa783f2..45eae2e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -26,7 +26,6 @@
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
 import com.android.wm.shell.flicker.appWindowBecomesInvisible
 import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
@@ -35,7 +34,6 @@
 import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
 import com.android.wm.shell.flicker.splitScreenDismissed
 import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
-import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -95,7 +93,9 @@
     fun primaryAppBoundsBecomesInvisible() = testSpec.splitAppLayerBoundsBecomesInvisible(
         primaryApp, landscapePosLeft = tapl.isTablet, portraitPosTop = false)
 
-    private fun secondaryAppBoundsIsFullscreenAtEnd_internal() {
+    @Presubmit
+    @Test
+    fun secondaryAppBoundsIsFullscreenAtEnd() {
         testSpec.assertLayers {
             this.isVisible(secondaryApp)
                 .isVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
@@ -117,20 +117,6 @@
 
     @Presubmit
     @Test
-    fun secondaryAppBoundsIsFullscreenAtEnd() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-        secondaryAppBoundsIsFullscreenAtEnd_internal()
-    }
-
-    @FlakyTest(bugId = 250528485)
-    @Test
-    fun secondaryAppBoundsIsFullscreenAtEnd_shellTransit() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
-        secondaryAppBoundsIsFullscreenAtEnd_internal()
-    }
-
-    @Presubmit
-    @Test
     fun primaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(primaryApp)
 
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index 84a8c0a..73159c9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.flicker.splitscreen
 
-import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.IwTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
@@ -146,19 +145,15 @@
         // robust enough to get the correct end state.
     }
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun splitScreenDividerKeepVisible() = testSpec.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun secondaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(secondaryApp)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
         primaryApp,
@@ -166,9 +161,6 @@
         portraitPosTop = true
     )
 
-    // TODO(b/246490534): Move back to presubmit after withAppTransitionIdle is robust enough to
-    // get the correct end state.
-    @FlakyTest(bugId = 246490534)
     @Test
     fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
         secondaryApp,
@@ -176,11 +168,9 @@
         portraitPosTop = false
     )
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp)
 
-    @FlakyTest(bugId = 241524174)
     @Test
     fun secondaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(secondaryApp)
 
diff --git a/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
new file mode 100644
index 0000000..8949a75
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/res/values/dimen.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<resources>
+    <!-- Resources used in WindowDecorationTests -->
+    <dimen name="test_freeform_decor_caption_height">32dp</dimen>
+    <dimen name="test_freeform_decor_caption_width">216dp</dimen>
+    <dimen name="test_window_decor_left_outset">10dp</dimen>
+    <dimen name="test_window_decor_top_outset">20dp</dimen>
+    <dimen name="test_window_decor_right_outset">30dp</dimen>
+    <dimen name="test_window_decor_bottom_outset">40dp</dimen>
+    <dimen name="test_window_decor_shadow_radius">5dp</dimen>
+    <dimen name="test_window_decor_resize_handle">10dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 6484b07..7896247 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -128,6 +128,7 @@
                 mShellExecutor, new Handler(mTestableLooper.getLooper()),
                 mActivityTaskManager, mContext,
                 mContentResolver, mTransitions);
+        mController.setEnableUAnimation(true);
         mShellInit.init();
         mEventTime = 0;
         mShellExecutor.flushAll();
@@ -206,10 +207,9 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
-        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
-        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(backEventCaptor.capture());
+        verify(mIOnBackInvokedCallback, atLeastOnce()).onBackProgressed(any(BackEvent.class));
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
@@ -236,11 +236,11 @@
 
         triggerBackGesture();
 
-        verify(appCallback, never()).onBackStarted();
+        verify(appCallback, never()).onBackStarted(any(BackEvent.class));
         verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
         verify(appCallback, times(1)).onBackInvoked();
 
-        verify(mIOnBackInvokedCallback, never()).onBackStarted();
+        verify(mIOnBackInvokedCallback, never()).onBackStarted(any(BackEvent.class));
         verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
         verify(mIOnBackInvokedCallback, never()).onBackInvoked();
         verify(mBackAnimationRunner, never()).onAnimationStart(
@@ -279,7 +279,7 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
     }
 
@@ -301,9 +301,8 @@
 
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
-
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
     }
 
 
@@ -318,7 +317,7 @@
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
 
         simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME);
-        verify(mIOnBackInvokedCallback).onBackStarted();
+        verify(mIOnBackInvokedCallback).onBackStarted(any(BackEvent.class));
         verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
 
         // Check that back invocation is dispatched.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
new file mode 100644
index 0000000..3aefc3f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.back;
+
+import static org.junit.Assert.assertEquals;
+
+import android.window.BackEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class TouchTrackerTest {
+    private static final float FAKE_THRESHOLD = 400;
+    private static final float INITIAL_X_LEFT_EDGE = 5;
+    private static final float INITIAL_X_RIGHT_EDGE = FAKE_THRESHOLD - INITIAL_X_LEFT_EDGE;
+    private TouchTracker mTouchTracker;
+
+    @Before
+    public void setUp() throws Exception {
+        mTouchTracker = new TouchTracker();
+        mTouchTracker.setProgressThreshold(FAKE_THRESHOLD);
+    }
+
+    @Test
+    public void generatesProgress_onStart() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
+        BackEvent event = mTouchTracker.createStartEvent(null);
+        assertEquals(event.getProgress(), 0f, 0f);
+    }
+
+    @Test
+    public void generatesProgress_leftEdge() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0, BackEvent.EDGE_LEFT);
+        float touchX = 10;
+
+        // Pre-commit
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+
+        // Post-commit
+        touchX += 100;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+
+        // Cancel
+        touchX -= 10;
+        mTouchTracker.setTriggerBack(false);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Cancel more
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restart
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restarted, but pre-commit
+        float restartX = touchX;
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - restartX) / FAKE_THRESHOLD, 0f);
+
+        // Restarted, post-commit
+        touchX += 10;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (touchX - INITIAL_X_LEFT_EDGE) / FAKE_THRESHOLD, 0f);
+    }
+
+    @Test
+    public void generatesProgress_rightEdge() {
+        mTouchTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0, BackEvent.EDGE_RIGHT);
+        float touchX = INITIAL_X_RIGHT_EDGE - 10; // Fake right edge
+
+        // Pre-commit
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Post-commit
+        touchX -= 100;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Cancel
+        touchX += 10;
+        mTouchTracker.setTriggerBack(false);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Cancel more
+        touchX += 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restart
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), 0, 0f);
+
+        // Restarted, but pre-commit
+        float restartX = touchX;
+        touchX -= 10;
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (restartX - touchX) / FAKE_THRESHOLD, 0f);
+
+        // Restarted, post-commit
+        touchX -= 10;
+        mTouchTracker.setTriggerBack(true);
+        mTouchTracker.update(touchX, 0);
+        assertEquals(getProgress(), (INITIAL_X_RIGHT_EDGE - touchX) / FAKE_THRESHOLD, 0f);
+    }
+
+    private float getProgress() {
+        return mTouchTracker.createProgressEvent().getProgress();
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 9967e5f..a6f19e7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -39,7 +39,6 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.view.IInputMethodManager;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -56,8 +55,6 @@
     @Mock
     private SurfaceControl.Transaction mT;
     @Mock
-    private IInputMethodManager mMock;
-    @Mock
     private ShellInit mShellInit;
     private DisplayImeController.PerDisplay mPerDisplay;
     private Executor mExecutor;
@@ -77,10 +74,6 @@
             }
         }, mExecutor) {
             @Override
-            public IInputMethodManager getImms() {
-                return mMock;
-            }
-            @Override
             void removeImeSurface() { }
         }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index c850a3b..79b520c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -20,6 +20,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -35,10 +37,12 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.testing.AndroidTestingRunner;
 import android.window.DisplayAreaInfo;
+import android.window.TransitionRequestInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransaction.Change;
@@ -243,6 +247,44 @@
         assertThat(op2.getContainer()).isEqualTo(token2.binder());
     }
 
+    @Test
+    public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() {
+        when(DesktopModeStatus.isActive(any())).thenReturn(false);
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_notTransitOpen_returnsNull() {
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_TO_FRONT, null /* trigger */, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_notFreeform_returnsNull() {
+        ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+        trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_returnsWct() {
+        ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+        trigger.token = new MockToken().mToken;
+        trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        WindowContainerTransaction wct = mController.handleRequest(
+                mock(IBinder.class),
+                new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */));
+        assertThat(wct).isNotNull();
+    }
+
     private static class MockToken {
         private final WindowContainerToken mToken;
         private final IBinder mBinder;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 9b28d11..aaa5c8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -38,7 +39,7 @@
     @Test
     fun addActiveTask_listenerNotifiedAndTaskIsActive() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
@@ -48,7 +49,7 @@
     @Test
     fun addActiveTask_sameTaskDoesNotNotify() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.addActiveTask(1)
@@ -58,7 +59,7 @@
     @Test
     fun addActiveTask_multipleTasksAddedNotifiesForEach() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.addActiveTask(2)
@@ -68,7 +69,7 @@
     @Test
     fun removeActiveTask_listenerNotifiedAndTaskNotActive() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.removeActiveTask(1)
@@ -80,7 +81,7 @@
     @Test
     fun removeActiveTask_removeNotExistingTaskDoesNotNotify() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
         repo.removeActiveTask(99)
         assertThat(listener.activeTaskChangedCalls).isEqualTo(0)
     }
@@ -90,10 +91,69 @@
         assertThat(repo.isActiveTask(99)).isFalse()
     }
 
-    class TestListener : DesktopModeTaskRepository.Listener {
+    @Test
+    fun addListener_notifiesVisibleFreeformTask() {
+        repo.updateVisibleFreeformTasks(1, true)
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1)
+    }
+
+    @Test
+    fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.updateVisibleFreeformTasks(1, true)
+        repo.updateVisibleFreeformTasks(2, true)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        // Equal to 2 because adding the listener notifies the current state
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+    }
+
+    @Test
+    fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.updateVisibleFreeformTasks(1, true)
+        repo.updateVisibleFreeformTasks(2, true)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        repo.updateVisibleFreeformTasks(1, false)
+        executor.flushAll()
+
+        // Equal to 2 because adding the listener notifies the current state
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+
+        repo.updateVisibleFreeformTasks(2, false)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isFalse()
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
+    }
+
+    class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
         var activeTaskChangedCalls = 0
         override fun onActiveTasksChanged() {
             activeTaskChangedCalls++
         }
     }
+
+    class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
+        var hasVisibleFreeformTasks = false
+        var visibleFreeformTaskChangedCalls = 0
+
+        override fun onVisibilityChanged(hasVisibleTasks: Boolean) {
+            hasVisibleFreeformTasks = hasVisibleTasks
+            visibleFreeformTaskChangedCalls++
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 103c8da..4d37e5d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -50,12 +50,13 @@
 import android.window.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.tests.R;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -145,8 +146,11 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
-                R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -196,13 +200,11 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-//        int outsetLeftId = R.dimen.split_divider_bar_width;
-//        int outsetTopId = R.dimen.gestures_onehanded_drag_threshold;
-//        int outsetRightId = R.dimen.freeform_resize_handle;
-//        int outsetBottomId = R.dimen.bubble_dismiss_target_padding_x;
-//        mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
-        mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
-                R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
 
@@ -211,8 +213,8 @@
         verify(decorContainerSurfaceBuilder).setParent(taskSurface);
         verify(decorContainerSurfaceBuilder).setContainerLayer();
         verify(mMockSurfaceControlStartT).setTrustedOverlay(decorContainerSurface, true);
-        verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -60, -60);
-        verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 420, 220);
+        verify(mMockSurfaceControlStartT).setPosition(decorContainerSurface, -20, -40);
+        verify(mMockSurfaceControlStartT).setWindowCrop(decorContainerSurface, 380, 220);
 
         verify(taskBackgroundSurfaceBuilder).setParent(taskSurface);
         verify(taskBackgroundSurfaceBuilder).setEffectLayer();
@@ -225,36 +227,34 @@
 
         verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
         verify(captionContainerSurfaceBuilder).setContainerLayer();
-        verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -6, -156);
-        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 432);
+        verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8);
+        verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
         verify(mMockSurfaceControlStartT).show(captionContainerSurface);
 
         verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
 
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
-                        argThat(lp -> lp.height == 432
+                        argThat(lp -> lp.height == 64
                                 && lp.width == 432
                                 && (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
         if (ViewRootImpl.CAPTION_ON_SHELL) {
             verify(mMockView).setTaskFocusState(true);
             verify(mMockWindowContainerTransaction)
                     .addRectInsetsProvider(taskInfo.token,
-                            new Rect(100, 300, 400, 516),
+                            new Rect(100, 300, 400, 332),
                             new int[] { InsetsState.ITYPE_CAPTION_BAR });
         }
 
         verify(mMockSurfaceControlFinishT)
                 .setPosition(taskSurface, TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y);
         verify(mMockSurfaceControlFinishT)
-                .setCrop(taskSurface, new Rect(-60, -60, 360, 160));
+                .setCrop(taskSurface, new Rect(-20, -40, 360, 180));
         verify(mMockSurfaceControlStartT)
                 .show(taskSurface);
 
-        assertEquals(420, mRelayoutResult.mWidth);
+        assertEquals(380, mRelayoutResult.mWidth);
         assertEquals(220, mRelayoutResult.mHeight);
-
-
     }
 
     @Test
@@ -293,8 +293,11 @@
         // Density is 2. Outsets are (20, 40, 60, 80) px. Shadow radius is 10px. Caption height is
         // 64px.
         taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
-        mRelayoutParams.setOutsets(R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle,
-                R.dimen.freeform_resize_handle, R.dimen.freeform_resize_handle);
+        mRelayoutParams.setOutsets(
+                R.dimen.test_window_decor_left_outset,
+                R.dimen.test_window_decor_top_outset,
+                R.dimen.test_window_decor_right_outset,
+                R.dimen.test_window_decor_bottom_outset);
 
         final SurfaceControl taskSurface = mock(SurfaceControl.class);
         final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
@@ -365,7 +368,8 @@
 
     private TestWindowDecoration createWindowDecoration(
             ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
-        return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
+        return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
+                mMockDisplayController, mMockShellTaskOrganizer,
                 taskInfo, testSurface,
                 new MockObjectSupplier<>(mMockSurfaceControlBuilders,
                         () -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
@@ -417,12 +421,10 @@
 
         @Override
         void relayout(ActivityManager.RunningTaskInfo taskInfo) {
-
             mRelayoutParams.mLayoutResId = 0;
-            mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_width;
-            mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width;
-            mRelayoutParams.mShadowRadiusId =
-                    R.dimen.freeform_decor_shadow_unfocused_thickness;
+            mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
+            mRelayoutParams.mCaptionWidthId = R.dimen.test_freeform_decor_caption_width;
+            mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
 
             relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
                     mMockWindowContainerTransaction, mMockView, mRelayoutResult);
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 46fbe7c..5e8a623 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -1020,40 +1020,6 @@
     return base::unexpected(std::nullopt);
 }
 
-template <typename TChar, typename SP>
-base::expected<size_t, NullOrIOError> ResStringPool::stringIndex(
-        SP sp, std::unordered_map<SP, size_t>& map) const
-{
-    AutoMutex lock(mStringIndexLock);
-
-    if (map.empty()) {
-        // build string index on the first call
-        for (size_t i = 0; i < mHeader->stringCount; i++) {
-            base::expected<SP, NullOrIOError> s;
-            if constexpr(std::is_same_v<TChar, char16_t>) {
-                s = stringAt(i);
-            } else {
-                s = string8At(i);
-            }
-            if (s.has_value()) {
-                const auto r = map.insert({*s, i});
-                if (!r.second) {
-                    ALOGE("failed to build string index, string id=%zu\n", i);
-                }
-            } else {
-                return base::unexpected(s.error());
-            }
-        }
-    }
-
-    if (!map.empty()) {
-        const auto result = map.find(sp);
-        if (result != map.end())
-            return result->second;
-    }
-    return base::unexpected(std::nullopt);
-}
-
 base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_t* str,
                                                                    size_t strLen) const
 {
@@ -1061,28 +1027,134 @@
         return base::unexpected(std::nullopt);
     }
 
-    if (kDebugStringPoolNoisy) {
-        ALOGI("indexOfString (%s): %s", isUTF8() ? "UTF-8" : "UTF-16",
-                String8(str, strLen).string());
-    }
-
-    base::expected<size_t, NullOrIOError> idx;
-    if (isUTF8()) {
-        auto str8 = String8(str, strLen);
-        idx = stringIndex<char>(StringPiece(str8.c_str(), str8.size()), mStringIndex8);
-    } else {
-        idx = stringIndex<char16_t>(StringPiece16(str, strLen), mStringIndex16);
-    }
-
-    if (UNLIKELY(!idx.has_value())) {
-        return base::unexpected(idx.error());
-    }
-
-    if (*idx < mHeader->stringCount) {
+    if ((mHeader->flags&ResStringPool_header::UTF8_FLAG) != 0) {
         if (kDebugStringPoolNoisy) {
-            ALOGI("MATCH! (idx=%zu)", *idx);
+            ALOGI("indexOfString UTF-8: %s", String8(str, strLen).string());
         }
-        return *idx;
+
+        // The string pool contains UTF 8 strings; we don't want to cause
+        // temporary UTF-16 strings to be created as we search.
+        if (mHeader->flags&ResStringPool_header::SORTED_FLAG) {
+            // Do a binary search for the string...  this is a little tricky,
+            // because the strings are sorted with strzcmp16().  So to match
+            // the ordering, we need to convert strings in the pool to UTF-16.
+            // But we don't want to hit the cache, so instead we will have a
+            // local temporary allocation for the conversions.
+            size_t convBufferLen = strLen + 4;
+            std::vector<char16_t> convBuffer(convBufferLen);
+            ssize_t l = 0;
+            ssize_t h = mHeader->stringCount-1;
+
+            ssize_t mid;
+            while (l <= h) {
+                mid = l + (h - l)/2;
+                int c = -1;
+                const base::expected<StringPiece, NullOrIOError> s = string8At(mid);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                if (s.has_value()) {
+                    char16_t* end = utf8_to_utf16(reinterpret_cast<const uint8_t*>(s->data()),
+                                                  s->size(), convBuffer.data(), convBufferLen);
+                    c = strzcmp16(convBuffer.data(), end-convBuffer.data(), str, strLen);
+                }
+                if (kDebugStringPoolNoisy) {
+                    ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n",
+                          s->data(), c, (int)l, (int)mid, (int)h);
+                }
+                if (c == 0) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("MATCH!");
+                    }
+                    return mid;
+                } else if (c < 0) {
+                    l = mid + 1;
+                } else {
+                    h = mid - 1;
+                }
+            }
+        } else {
+            // It is unusual to get the ID from an unsorted string block...
+            // most often this happens because we want to get IDs for style
+            // span tags; since those always appear at the end of the string
+            // block, start searching at the back.
+            String8 str8(str, strLen);
+            const size_t str8Len = str8.size();
+            for (int i=mHeader->stringCount-1; i>=0; i--) {
+                const base::expected<StringPiece, NullOrIOError> s = string8At(i);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                if (s.has_value()) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("Looking at %s, i=%d\n", s->data(), i);
+                    }
+                    if (str8Len == s->size()
+                            && memcmp(s->data(), str8.string(), str8Len) == 0) {
+                        if (kDebugStringPoolNoisy) {
+                            ALOGI("MATCH!");
+                        }
+                        return i;
+                    }
+                }
+            }
+        }
+
+    } else {
+        if (kDebugStringPoolNoisy) {
+            ALOGI("indexOfString UTF-16: %s", String8(str, strLen).string());
+        }
+
+        if (mHeader->flags&ResStringPool_header::SORTED_FLAG) {
+            // Do a binary search for the string...
+            ssize_t l = 0;
+            ssize_t h = mHeader->stringCount-1;
+
+            ssize_t mid;
+            while (l <= h) {
+                mid = l + (h - l)/2;
+                const base::expected<StringPiece16, NullOrIOError> s = stringAt(mid);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                int c = s.has_value() ? strzcmp16(s->data(), s->size(), str, strLen) : -1;
+                if (kDebugStringPoolNoisy) {
+                    ALOGI("Looking at %s, cmp=%d, l/mid/h=%d/%d/%d\n",
+                          String8(s->data(), s->size()).string(), c, (int)l, (int)mid, (int)h);
+                }
+                if (c == 0) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("MATCH!");
+                    }
+                    return mid;
+                } else if (c < 0) {
+                    l = mid + 1;
+                } else {
+                    h = mid - 1;
+                }
+            }
+        } else {
+            // It is unusual to get the ID from an unsorted string block...
+            // most often this happens because we want to get IDs for style
+            // span tags; since those always appear at the end of the string
+            // block, start searching at the back.
+            for (int i=mHeader->stringCount-1; i>=0; i--) {
+                const base::expected<StringPiece16, NullOrIOError> s = stringAt(i);
+                if (UNLIKELY(IsIOError(s))) {
+                    return base::unexpected(s.error());
+                }
+                if (kDebugStringPoolNoisy) {
+                    ALOGI("Looking at %s, i=%d\n", String8(s->data(), s->size()).string(), i);
+                }
+                if (s.has_value() && strLen == s->size() &&
+                        strzcmp16(s->data(), s->size(), str, strLen) == 0) {
+                    if (kDebugStringPoolNoisy) {
+                        ALOGI("MATCH!");
+                    }
+                    return i;
+                }
+            }
+        }
     }
     return base::unexpected(std::nullopt);
 }
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 24628cd..9309091 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -41,7 +41,6 @@
 #include <array>
 #include <map>
 #include <memory>
-#include <unordered_map>
 
 namespace android {
 
@@ -563,17 +562,8 @@
     incfs::map_ptr<uint32_t>                      mStyles;
     uint32_t                                      mStylePoolSize;    // number of uint32_t
 
-    // mStringIndex is used to quickly map a string to its ID
-    mutable Mutex                                       mStringIndexLock;
-    mutable std::unordered_map<StringPiece, size_t>     mStringIndex8;
-    mutable std::unordered_map<StringPiece16, size_t>   mStringIndex16;
-
     base::expected<StringPiece, NullOrIOError> stringDecodeAt(
         size_t idx, incfs::map_ptr<uint8_t> str, size_t encLen) const;
-
-    template <typename TChar, typename SP=BasicStringPiece<TChar>>
-    base::expected<size_t, NullOrIOError> stringIndex(
-        SP str, std::unordered_map<SP, size_t>& map) const;
 };
 
 /**
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index d0d24a8..673041a 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -15,19 +15,20 @@
  */
 
 #include "CanvasTransform.h"
-#include "Properties.h"
-#include "utils/Color.h"
 
+#include <SkAndroidFrameworkUtils.h>
 #include <SkColorFilter.h>
 #include <SkGradientShader.h>
+#include <SkHighContrastFilter.h>
 #include <SkPaint.h>
 #include <SkShader.h>
+#include <log/log.h>
 
 #include <algorithm>
 #include <cmath>
 
-#include <log/log.h>
-#include <SkHighContrastFilter.h>
+#include "Properties.h"
+#include "utils/Color.h"
 
 namespace android::uirenderer {
 
@@ -82,27 +83,21 @@
     paint.setColor(newColor);
 
     if (paint.getShader()) {
-        SkShader::GradientInfo info;
+        SkAndroidFrameworkUtils::LinearGradientInfo info;
         std::array<SkColor, 10> _colorStorage;
         std::array<SkScalar, _colorStorage.size()> _offsetStorage;
         info.fColorCount = _colorStorage.size();
         info.fColors = _colorStorage.data();
         info.fColorOffsets = _offsetStorage.data();
-        SkShader::GradientType type = paint.getShader()->asAGradient(&info);
 
-        if (info.fColorCount <= 10) {
-            switch (type) {
-                case SkShader::kLinear_GradientType:
-                    for (int i = 0; i < info.fColorCount; i++) {
-                        info.fColors[i] = transformColor(transform, info.fColors[i]);
-                    }
-                    paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors,
-                                                                 info.fColorOffsets, info.fColorCount,
-                                                                 info.fTileMode, info.fGradientFlags, nullptr));
-                    break;
-                default:break;
+        if (SkAndroidFrameworkUtils::ShaderAsALinearGradient(paint.getShader(), &info) &&
+            info.fColorCount <= _colorStorage.size()) {
+            for (int i = 0; i < info.fColorCount; i++) {
+                info.fColors[i] = transformColor(transform, info.fColors[i]);
             }
-
+            paint.setShader(SkGradientShader::MakeLinear(
+                    info.fPoints, info.fColors, info.fColorOffsets, info.fColorCount,
+                    info.fTileMode, info.fGradientFlags, nullptr));
         }
     }
 
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2547a963..d975e96 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -6632,8 +6632,8 @@
             }
         }
         if (k == ports.size()) {
-            // this hould never happen
-            Log.e(TAG, "updatePortConfig port not found for handle: "+port.handle().id());
+            // This can happen in case of stale audio patch referring to a removed device and is
+            // handled by the caller.
             return null;
         }
         AudioGainConfig gainCfg = portCfg.gain();
diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java
index 39b3d0b..0291f64 100644
--- a/media/java/android/media/ImageWriter.java
+++ b/media/java/android/media/ImageWriter.java
@@ -264,10 +264,9 @@
         if (useSurfaceImageFormatInfo) {
             // nativeInit internally overrides UNKNOWN format. So does surface format query after
             // nativeInit and before getEstimatedNativeAllocBytes().
-            imageFormat = SurfaceUtils.getSurfaceFormat(surface);
-            mDataSpace = dataSpace = PublicFormatUtils.getHalDataspace(imageFormat);
-            mHardwareBufferFormat =
-                hardwareBufferFormat = PublicFormatUtils.getHalFormat(imageFormat);
+            mHardwareBufferFormat = hardwareBufferFormat = SurfaceUtils.getSurfaceFormat(surface);
+            mDataSpace = dataSpace = SurfaceUtils.getSurfaceDataspace(surface);
+            imageFormat = PublicFormatUtils.getPublicFormat(hardwareBufferFormat, dataSpace);
         }
 
         // Estimate the native buffer allocation size and register it so it gets accounted for
diff --git a/media/java/android/media/midi/MidiManager.java b/media/java/android/media/midi/MidiManager.java
index 74c5499..ee82588 100644
--- a/media/java/android/media/midi/MidiManager.java
+++ b/media/java/android/media/midi/MidiManager.java
@@ -240,8 +240,7 @@
      * @param handler The {@link android.os.Handler Handler} that will be used for delivering the
      *                device notifications. If handler is null, then the thread used for the
      *                callback is unspecified.
-     * @deprecated Use the {@link #registerDeviceCallback}
-     *             method with Executor and transport instead.
+     * @deprecated Use {@link #registerDeviceCallback(int, Executor, DeviceCallback)} instead.
      */
     @Deprecated
     public void registerDeviceCallback(DeviceCallback callback, Handler handler) {
diff --git a/opengl/java/android/opengl/GLSurfaceView.java b/opengl/java/android/opengl/GLSurfaceView.java
index 75131b0..4738318 100644
--- a/opengl/java/android/opengl/GLSurfaceView.java
+++ b/opengl/java/android/opengl/GLSurfaceView.java
@@ -1667,7 +1667,15 @@
                 mWantRenderNotification = true;
                 mRequestRender = true;
                 mRenderComplete = false;
-                mFinishDrawingRunnable = finishDrawing;
+                final Runnable oldCallback = mFinishDrawingRunnable;
+                mFinishDrawingRunnable = () -> {
+                    if (oldCallback != null) {
+                        oldCallback.run();
+                    }
+                    if (finishDrawing != null) {
+                        finishDrawing.run();
+                    }
+                };
 
                 sGLThreadManager.notifyAll();
             }
diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp
index fc753da..6990ad0 100644
--- a/packages/CarrierDefaultApp/Android.bp
+++ b/packages/CarrierDefaultApp/Android.bp
@@ -10,6 +10,7 @@
 android_app {
     name: "CarrierDefaultApp",
     srcs: ["src/**/*.java"],
+    libs: ["SliceStore"],
     platform_apis: true,
     certificate: "platform",
 }
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
index 632dfb3..a5b104b 100644
--- a/packages/CarrierDefaultApp/AndroidManifest.xml
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <uses-permission android:name="android.permission.NETWORK_BYPASS_PRIVATE_DNS" />
     <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
 
     <application
         android:label="@string/app_name"
@@ -71,5 +72,22 @@
                 <data android:host="*" />
             </intent-filter>
         </activity-alias>
+
+        <receiver android:name="com.android.carrierdefaultapp.SliceStoreBroadcastReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.phone.slicestore.action.START_SLICE_STORE" />
+                <action android:name="com.android.phone.slicestore.action.SLICE_STORE_RESPONSE_TIMEOUT" />
+                <action android:name="com.android.phone.slicestore.action.NOTIFICATION_CANCELED" />
+            </intent-filter>
+        </receiver>
+        <activity android:name="com.android.carrierdefaultapp.SliceStoreActivity"
+                  android:label="@string/slice_store_label"
+                  android:exported="true"
+                  android:configChanges="keyboardHidden|orientation|screenSize">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
new file mode 100644
index 0000000..ad8a21c
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/drawable/ic_network_boost.xml
@@ -0,0 +1,23 @@
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path android:fillColor="@android:color/white"
+          android:pathData="M3,17V15H8Q8,15 8,15Q8,15 8,15V13Q8,13 8,13Q8,13 8,13H3V7H10V9H5V11H8Q8.825,11 9.413,11.587Q10,12.175 10,13V15Q10,15.825 9.413,16.413Q8.825,17 8,17ZM21,11V15Q21,15.825 20.413,16.413Q19.825,17 19,17H14Q13.175,17 12.588,16.413Q12,15.825 12,15V9Q12,8.175 12.588,7.587Q13.175,7 14,7H19Q19.825,7 20.413,7.587Q21,8.175 21,9H14Q14,9 14,9Q14,9 14,9V15Q14,15 14,15Q14,15 14,15H19Q19,15 19,15Q19,15 19,15V13H16.5V11Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
index 65a7cec..ce88a40 100644
--- a/packages/CarrierDefaultApp/res/values/strings.xml
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -13,4 +13,18 @@
     <string name="ssl_error_warning">The network you&#8217;re trying to join has security issues.</string>
     <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
     <string name="ssl_error_continue">Continue anyway via browser</string>
+
+    <!-- Telephony notification channel name for network boost notifications. -->
+    <string name="network_boost_notification_channel">Network Boost</string>
+    <!-- Notification title text for the network boost notification. -->
+    <string name="network_boost_notification_title">%s recommends a data boost</string>
+    <!-- Notification detail text for the network boost notification. -->
+    <string name="network_boost_notification_detail">Buy a network boost for better performance</string>
+    <!-- Notification button text to cancel the network boost notification. -->
+    <string name="network_boost_notification_button_not_now">Not now</string>
+    <!-- Notification button text to manage the network boost notification. -->
+    <string name="network_boost_notification_button_manage">Manage</string>
+
+    <!-- Label to display when the slice store opens. -->
+    <string name="slice_store_label">Purchase a network boost.</string>
 </resources>
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
new file mode 100644
index 0000000..24cb5f9
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.phone.slicestore.SliceStore;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Activity that launches when the user clicks on the network boost notification.
+ */
+public class SliceStoreActivity extends Activity {
+    private static final String TAG = "SliceStoreActivity";
+
+    private URL mUrl;
+    private WebView mWebView;
+    private int mPhoneId;
+    private int mSubId;
+    private @TelephonyManager.PremiumCapability int mCapability;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        mPhoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        mSubId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        mCapability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        mUrl = getUrl();
+        logd("onCreate: mPhoneId=" + mPhoneId + ", mSubId=" + mSubId + ", mCapability="
+                + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+                + ", mUrl=" + mUrl);
+        getApplicationContext().getSystemService(NotificationManager.class)
+                .cancel(SliceStoreBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
+        if (!SliceStoreBroadcastReceiver.isIntentValid(intent)) {
+            loge("Not starting SliceStoreActivity with an invalid Intent: " + intent);
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mUrl == null) {
+            loge("Unable to create a URL from carrier configs.");
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR);
+            finishAndRemoveTask();
+            return;
+        }
+        if (mSubId != SubscriptionManager.getDefaultSubscriptionId()) {
+            loge("Unable to start SliceStore on the non-default data subscription: " + mSubId);
+            SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                    intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+            finishAndRemoveTask();
+            return;
+        }
+
+        SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this);
+
+        mWebView = new WebView(getApplicationContext());
+        setContentView(mWebView);
+        mWebView.loadUrl(mUrl.toString());
+        // TODO(b/245882601): Get back response from WebView
+    }
+
+    @Override
+    protected void onDestroy() {
+        logd("onDestroy: User canceled the purchase by closing the application.");
+        SliceStoreBroadcastReceiver.sendSliceStoreResponse(
+                getIntent(), SliceStore.EXTRA_INTENT_CANCELED);
+        SliceStoreBroadcastReceiver.removeSliceStoreActivity(mCapability);
+        super.onDestroy();
+    }
+
+    private @Nullable URL getUrl() {
+        String url = getApplicationContext().getSystemService(CarrierConfigManager.class)
+                .getConfigForSubId(mSubId).getString(
+                        CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
+        try {
+            return new URL(url);
+        } catch (MalformedURLException e) {
+            loge("Invalid URL: " + url);
+        }
+        return null;
+    }
+
+    private static void logd(@NonNull String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(@NonNull String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
new file mode 100644
index 0000000..7eb851d
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebView;
+
+import com.android.phone.slicestore.SliceStore;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The SliceStoreBroadcastReceiver listens for {@link SliceStore#ACTION_START_SLICE_STORE} from the
+ * SliceStore in the phone process to start the SliceStore application. It displays the network
+ * boost notification to the user and will start the {@link SliceStoreActivity} to display the
+ * {@link WebView} to purchase network boosts from the user's carrier.
+ */
+public class SliceStoreBroadcastReceiver extends BroadcastReceiver{
+    private static final String TAG = "SliceStoreBroadcastReceiver";
+
+    /** Weak references to {@link SliceStoreActivity} for each capability, if it exists. */
+    private static final Map<Integer, WeakReference<SliceStoreActivity>> sSliceStoreActivities =
+            new HashMap<>();
+
+    /** Channel ID for the network boost notification. */
+    private static final String NETWORK_BOOST_NOTIFICATION_CHANNEL_ID = "network_boost";
+    /** Tag for the network boost notification. */
+    public static final String NETWORK_BOOST_NOTIFICATION_TAG = "SliceStore.Notification";
+    /** Action for when the user clicks the "Not now" button on the network boost notification. */
+    private static final String ACTION_NOTIFICATION_CANCELED =
+            "com.android.phone.slicestore.action.NOTIFICATION_CANCELED";
+
+    /**
+     * Create a weak reference to {@link SliceStoreActivity}. The reference will be removed when
+     * {@link SliceStoreActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     * @param sliceStoreActivity The instance of SliceStoreActivity.
+     */
+    public static void updateSliceStoreActivity(@TelephonyManager.PremiumCapability int capability,
+            @NonNull SliceStoreActivity sliceStoreActivity) {
+        sSliceStoreActivities.put(capability, new WeakReference<>(sliceStoreActivity));
+    }
+
+    /**
+     * Remove the weak reference to {@link SliceStoreActivity} when
+     * {@link SliceStoreActivity#onDestroy()} is called.
+     *
+     * @param capability The premium capability requested.
+     */
+    public static void removeSliceStoreActivity(
+            @TelephonyManager.PremiumCapability int capability) {
+        sSliceStoreActivities.remove(capability);
+    }
+
+    /**
+     * Send the PendingIntent containing the corresponding SliceStore response.
+     *
+     * @param intent The Intent containing the PendingIntent extra.
+     * @param extra The extra to get the PendingIntent to send.
+     */
+    public static void sendSliceStoreResponse(@NonNull Intent intent, @NonNull String extra) {
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("PendingIntent does not exist for extra: " + extra);
+            return;
+        }
+        try {
+            pendingIntent.send();
+        } catch (PendingIntent.CanceledException e) {
+            loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
+        }
+    }
+
+    /**
+     * Check whether the Intent is valid and can be used to complete purchases in the SliceStore.
+     * This checks that all necessary extras exist and that the values are valid.
+     *
+     * @param intent The intent to check
+     * @return {@code true} if the intent is valid and {@code false} otherwise.
+     */
+    public static boolean isIntentValid(@NonNull Intent intent) {
+        int phoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
+                SubscriptionManager.INVALID_PHONE_INDEX);
+        if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
+            loge("isIntentValid: invalid phone index: " + phoneId);
+            return false;
+        }
+
+        int subId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            loge("isIntentValid: invalid subscription ID: " + subId);
+            return false;
+        }
+
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        if (capability == SliceStore.PREMIUM_CAPABILITY_INVALID) {
+            loge("isIntentValid: invalid premium capability: " + capability);
+            return false;
+        }
+
+        String appName = intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME);
+        if (TextUtils.isEmpty(appName)) {
+            loge("isIntentValid: empty requesting application name: " + appName);
+            return false;
+        }
+
+        return isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CANCELED)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED)
+                && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+    }
+
+    private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
+        String intentType = getPendingIntentType(extra);
+        PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+        if (pendingIntent == null) {
+            loge("isPendingIntentValid: " + intentType + " intent not found.");
+            return false;
+        } else if (pendingIntent.getCreatorPackage().equals(TelephonyManager.PHONE_PROCESS_NAME)) {
+            return true;
+        }
+        loge("isPendingIntentValid: " + intentType + " intent was created by "
+                + pendingIntent.getCreatorPackage() + " instead of the phone process.");
+        return false;
+    }
+
+    @NonNull private static String getPendingIntentType(@NonNull String extra) {
+        switch (extra) {
+            case SliceStore.EXTRA_INTENT_CANCELED: return "canceled";
+            case SliceStore.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
+            case SliceStore.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
+            case SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA: return "not default data";
+            default: {
+                loge("Unknown pending intent extra: " + extra);
+                return "unknown(" + extra + ")";
+            }
+        }
+    }
+
+    @Override
+    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+        logd("onReceive intent: " + intent.getAction());
+        switch (intent.getAction()) {
+            case SliceStore.ACTION_START_SLICE_STORE:
+                onDisplayBoosterNotification(context, intent);
+                break;
+            case SliceStore.ACTION_SLICE_STORE_RESPONSE_TIMEOUT:
+                onTimeout(context, intent);
+                break;
+            case ACTION_NOTIFICATION_CANCELED:
+                onUserCanceled(context, intent);
+                break;
+            default:
+                loge("Received unknown action: " + intent.getAction());
+        }
+    }
+
+    private void onDisplayBoosterNotification(@NonNull Context context, @NonNull Intent intent) {
+        if (!isIntentValid(intent)) {
+            sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED);
+            return;
+        }
+
+        context.getSystemService(NotificationManager.class).createNotificationChannel(
+                new NotificationChannel(NETWORK_BOOST_NOTIFICATION_CHANNEL_ID,
+                        context.getResources().getString(
+                                R.string.network_boost_notification_channel),
+                        NotificationManager.IMPORTANCE_DEFAULT));
+
+        Notification notification =
+                new Notification.Builder(context, NETWORK_BOOST_NOTIFICATION_CHANNEL_ID)
+                        .setContentTitle(String.format(context.getResources().getString(
+                                R.string.network_boost_notification_title),
+                                intent.getStringExtra(SliceStore.EXTRA_REQUESTING_APP_NAME)))
+                        .setContentText(context.getResources().getString(
+                                R.string.network_boost_notification_detail))
+                        .setSmallIcon(R.drawable.ic_network_boost)
+                        .setContentIntent(createContentIntent(context, intent, 1))
+                        .setDeleteIntent(intent.getParcelableExtra(
+                                SliceStore.EXTRA_INTENT_CANCELED, PendingIntent.class))
+                        // Add an action for the "Not now" button, which has the same behavior as
+                        // the user canceling or closing the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_not_now),
+                                createCanceledIntent(context, intent)).build())
+                        // Add an action for the "Manage" button, which has the same behavior as
+                        // the user clicking on the notification.
+                        .addAction(new Notification.Action.Builder(
+                                Icon.createWithResource(context, R.drawable.ic_network_boost),
+                                context.getResources().getString(
+                                        R.string.network_boost_notification_button_manage),
+                                createContentIntent(context, intent, 2)).build())
+                        .build();
+
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("Display the booster notification for capability "
+                + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class).notifyAsUser(
+                NETWORK_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
+    }
+
+    /**
+     * Create the intent for when the user clicks on the "Manage" button on the network boost
+     * notification or the notification itself. This will open {@link SliceStoreActivity}.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the SliceStore application.
+     * @param requestCode The request code for the PendingIntent.
+     *
+     * @return The intent to start {@link SliceStoreActivity}.
+     */
+    @NonNull private PendingIntent createContentIntent(@NonNull Context context,
+            @NonNull Intent intent, int requestCode) {
+        Intent i = new Intent(context, SliceStoreActivity.class);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SliceStoreActivity"));
+        i.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
+                | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        i.putExtras(intent);
+        return PendingIntent.getActivityAsUser(context, requestCode, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, null /* options */,
+                UserHandle.CURRENT);
+    }
+
+    /**
+     * Create the canceled intent for when the user clicks the "Not now" button on the network boost
+     * notification. This will send {@link #ACTION_NOTIFICATION_CANCELED} and has the same function
+     * as if the user had canceled or removed the notification.
+     *
+     * @param context The Context to create the intent for.
+     * @param intent The source Intent used to launch the SliceStore application.
+     *
+     * @return The canceled intent.
+     */
+    @NonNull private PendingIntent createCanceledIntent(@NonNull Context context,
+            @NonNull Intent intent) {
+        Intent i = new Intent(ACTION_NOTIFICATION_CANCELED);
+        i.setComponent(ComponentName.unflattenFromString(
+                "com.android.carrierdefaultapp/.SliceStoreBroadcastReceiver"));
+        i.putExtras(intent);
+        return PendingIntent.getBroadcast(context, 0, i,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
+    }
+
+    private void onTimeout(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("Purchase capability " + TelephonyManager.convertPremiumCapabilityToString(capability)
+                + " timed out.");
+        if (sSliceStoreActivities.get(capability) == null) {
+            // Notification is still active
+            logd("Closing booster notification since the user did not respond in time.");
+            context.getSystemService(NotificationManager.class).cancelAsUser(
+                    NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        } else {
+            // Notification was dismissed but SliceStoreActivity is still active
+            logd("Closing SliceStore WebView since the user did not complete the purchase "
+                    + "in time.");
+            sSliceStoreActivities.get(capability).get().finishAndRemoveTask();
+            // TODO: Display a toast to indicate timeout for better UX?
+        }
+    }
+
+    private void onUserCanceled(@NonNull Context context, @NonNull Intent intent) {
+        int capability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
+                SliceStore.PREMIUM_CAPABILITY_INVALID);
+        logd("onUserCanceled: " + TelephonyManager.convertPremiumCapabilityToString(capability));
+        context.getSystemService(NotificationManager.class)
+                .cancelAsUser(NETWORK_BOOST_NOTIFICATION_TAG, capability, UserHandle.ALL);
+        sendSliceStoreResponse(intent, SliceStore.EXTRA_INTENT_CANCELED);
+    }
+
+    private static void logd(String s) {
+        Log.d(TAG, s);
+    }
+
+    private static void loge(String s) {
+        Log.e(TAG, s);
+    }
+}
diff --git a/packages/CompanionDeviceManager/TEST_MAPPING b/packages/CompanionDeviceManager/TEST_MAPPING
deleted file mode 100644
index 63f54fa..0000000
--- a/packages/CompanionDeviceManager/TEST_MAPPING
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "CtsOsTestCases",
-      "options": [
-        {
-          "include-filter": "android.os.cts.CompanionDeviceManagerTest"
-        }
-      ]
-    }
-  ]
-}
diff --git a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
index eef3009..675072b 100644
--- a/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-zh-rTW/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_label" msgid="4470785958457506021">"隨附裝置管理員"</string>
+    <string name="app_label" msgid="4470785958457506021">"隨附裝置管理工具"</string>
     <string name="confirmation_title" msgid="3785000297483688997">"允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」&lt;strong&gt;&lt;/strong&gt;存取「<xliff:g id="DEVICE_NAME">%2$s</xliff:g>」&lt;strong&gt;&lt;/strong&gt;"</string>
     <string name="profile_name_watch" msgid="576290739483672360">"手錶"</string>
     <string name="chooser_title" msgid="2262294130493605839">"選擇要讓「<xliff:g id="APP_NAME">%2$s</xliff:g>」&lt;strong&gt;&lt;/strong&gt;管理的<xliff:g id="PROFILE_NAME">%1$s</xliff:g>"</string>
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 92ce772..7f843a2 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -10,10 +10,15 @@
   <string name="passkey_creation_intro_body">Use your fingerprint, face or screen lock to sign in with a unique passkey that can’t be forgotten or stolen. Learn more</string>
   <string name="choose_provider_title">Choose your default provider</string>
   <string name="choose_provider_body">This provider will store passkeys and passwords for you and help you easily autofill and sign in. Learn more</string>
-  <string name="choose_create_option_title">Create a passkey at</string>
+  <string name="choose_create_option_passkey_title">Create a passkey in <xliff:g id="providerInfoDisplayName">%1$s</xliff:g>?</string>
+  <string name="choose_create_option_password_title">Save your password to <xliff:g id="providerInfoDisplayName">%1$s</xliff:g>?</string>
+  <string name="choose_create_option_sign_in_title">Save your sign-in info to <xliff:g id="providerInfoDisplayName">%1$s</xliff:g>?</string>
   <string name="choose_sign_in_title">Use saved sign in</string>
   <string name="create_passkey_at">Create passkey at</string>
-  <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoName">%1$s</xliff:g> for all your sign-ins?</string>
+  <string name="use_provider_for_all_title">Use <xliff:g id="providerInfoDisplayName">%1$s</xliff:g> for all your sign-ins?</string>
   <string name="set_as_default">Set as default</string>
   <string name="use_once">Use once</string>
+  <string name="choose_create_option_description">You can use saved <xliff:g id="type">%1$s</xliff:g> on any device. It will be saved to <xliff:g id="providerInfoDisplayName">%2$s</xliff:g> for <xliff:g id="createInfoDisplayName">%3$s</xliff:g></string>
+  <string name="more_options_title_multiple_options"><xliff:g id="providerInfoDisplayName">%1$s</xliff:g> for <xliff:g id="createInfoTitle">%2$s</xliff:g></string>
+  <string name="more_options_title_one_option"><xliff:g id="providerInfoDisplayName">%1$s</xliff:g></string>
 </resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index b63f3c9..56fb1a9 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -16,24 +16,27 @@
 
 package com.android.credentialmanager
 
-import android.app.Activity
 import android.app.slice.Slice
 import android.app.slice.SliceSpec
 import android.content.Context
 import android.content.Intent
+import android.credentials.CreateCredentialRequest
 import android.credentials.ui.Constants
 import android.credentials.ui.Entry
 import android.credentials.ui.ProviderData
 import android.credentials.ui.RequestInfo
-import android.credentials.ui.UserSelectionResult
+import android.credentials.ui.BaseDialogResult
+import android.credentials.ui.UserSelectionDialogResult
 import android.graphics.drawable.Icon
 import android.os.Binder
 import android.os.Bundle
 import android.os.ResultReceiver
 import com.android.credentialmanager.createflow.CreatePasskeyUiState
 import com.android.credentialmanager.createflow.CreateScreenState
+import com.android.credentialmanager.createflow.RequestDisplayInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
 import com.android.credentialmanager.getflow.GetScreenState
+import com.android.credentialmanager.jetpack.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
 
 // Consider repo per screen, similar to view model?
 class CredentialManagerRepo(
@@ -49,11 +52,7 @@
     requestInfo = intent.extras?.getParcelable(
       RequestInfo.EXTRA_REQUEST_INFO,
       RequestInfo::class.java
-    ) ?: RequestInfo(
-      Binder(),
-      RequestInfo.TYPE_CREATE,
-      /*isFirstUsage=*/false
-    )
+    ) ?: testRequestInfo()
 
     providerList = intent.extras?.getParcelableArrayList(
       ProviderData.EXTRA_PROVIDER_DATA_LIST,
@@ -67,37 +66,51 @@
   }
 
   fun onCancel() {
-    resultReceiver?.send(Activity.RESULT_CANCELED, null)
+    val resultData = Bundle()
+    BaseDialogResult.addToBundle(BaseDialogResult(requestInfo.token), resultData)
+    resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_CANCELED, resultData)
   }
 
-  fun onOptionSelected(providerPackageName: String, entryId: Int) {
-    val userSelectionResult = UserSelectionResult(
+  fun onOptionSelected(providerPackageName: String, entryKey: String, entrySubkey: String) {
+    val userSelectionDialogResult = UserSelectionDialogResult(
       requestInfo.token,
       providerPackageName,
-      entryId
+      entryKey,
+      entrySubkey
     )
     val resultData = Bundle()
-    resultData.putParcelable(
-      UserSelectionResult.EXTRA_USER_SELECTION_RESULT,
-      userSelectionResult
-    )
-    resultReceiver?.send(Activity.RESULT_OK, resultData)
+    UserSelectionDialogResult.addToBundle(userSelectionDialogResult, resultData)
+    resultReceiver?.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, resultData)
   }
 
   fun getCredentialInitialUiState(): GetCredentialUiState {
     val providerList = GetFlowUtils.toProviderList(providerList, context)
+    // TODO: covert from real requestInfo
+    val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo(
+      "Elisa Beckett",
+      "beckett-bakert@gmail.com",
+      TYPE_PUBLIC_KEY_CREDENTIAL,
+      "tribank")
     return GetCredentialUiState(
       providerList,
       GetScreenState.CREDENTIAL_SELECTION,
+      requestDisplayInfo,
       providerList.first()
     )
   }
 
   fun createPasskeyInitialUiState(): CreatePasskeyUiState {
     val providerList = CreateFlowUtils.toProviderList(providerList, context)
+    // TODO: covert from real requestInfo
+    val requestDisplayInfo = RequestDisplayInfo(
+      "Elisa Beckett",
+      "beckett-bakert@gmail.com",
+      TYPE_PUBLIC_KEY_CREDENTIAL,
+      "tribank")
     return CreatePasskeyUiState(
       providers = providerList,
       currentScreenState = CreateScreenState.PASSKEY_INTRO,
+      requestDisplayInfo,
     )
   }
 
@@ -119,50 +132,53 @@
   // TODO: below are prototype functionalities. To be removed for productionization.
   private fun testProviderList(): List<ProviderData> {
     return listOf(
-      ProviderData(
+      ProviderData.Builder(
         "com.google",
-        listOf<Entry>(
-          newEntry(1, "elisa.beckett@gmail.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(2, "elisa.work@google.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(3, "Go to Settings", "",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(4, "Switch Account", "",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
+        "Google Password Manager",
+        Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
+        .setCredentialEntries(
+          listOf<Entry>(
+            newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
+              "Elisa Backett", "20 passwords and 7 passkeys saved"),
+            newEntry("key1", "subkey-2", "elisa.work@google.com",
+              "Elisa Backett Work", "20 passwords and 7 passkeys saved"),
+          )
+        ).setActionChips(
+          listOf<Entry>(
+            newEntry("key2", "subkey-1", "Go to Settings", "",
+                     "20 passwords and 7 passkeys saved"),
+            newEntry("key2", "subkey-2", "Switch Account", "",
+                     "20 passwords and 7 passkeys saved"),
+          ),
+        ).build(),
+      ProviderData.Builder(
         "com.dashlane",
-        listOf<Entry>(
-          newEntry(5, "elisa.beckett@dashlane.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-          newEntry(6, "elisa.work@dashlane.com", "Elisa Backett Work",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(
-          newEntry(7, "Manage Accounts", "Manage your accounts in the dashlane app",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        null
-      ),
-      ProviderData(
-        "com.lastpass",
-        listOf<Entry>(
-          newEntry(8, "elisa.beckett@lastpass.com", "Elisa Backett",
-            "20 passwords and 7 passkeys saved"),
-        ),
-        listOf<Entry>(),
-        null
-      )
-
+        "Dashlane",
+        Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
+        .setCredentialEntries(
+          listOf<Entry>(
+            newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
+              "Elisa Backett", "20 passwords and 7 passkeys saved"),
+            newEntry("key1", "subkey-4", "elisa.work@dashlane.com",
+              "Elisa Backett Work", "20 passwords and 7 passkeys saved"),
+          )
+        ).setActionChips(
+          listOf<Entry>(
+            newEntry("key2", "subkey-3", "Manage Accounts",
+              "Manage your accounts in the dashlane app",
+                     "20 passwords and 7 passkeys saved"),
+          ),
+        ).build(),
     )
   }
 
-  private fun newEntry(id: Int, title: String, subtitle: String, usageData: String): Entry {
+  private fun newEntry(
+    key: String,
+    subkey: String,
+    title: String,
+    subtitle: String,
+    usageData: String
+  ): Entry {
     val slice = Slice.Builder(
       Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(Entry.VERSION, 1)
     )
@@ -175,8 +191,23 @@
       .addText(usageData, Slice.SUBTYPE_MESSAGE, listOf(Entry.HINT_SUBTITLE))
       .build()
     return Entry(
-      id,
+      key,
+      subkey,
       slice
     )
   }
+
+  private fun testRequestInfo(): RequestInfo {
+    val data = Bundle()
+    return RequestInfo.newCreateRequestInfo(
+      Binder(),
+      CreateCredentialRequest(
+        // TODO: use the jetpack type and utils once defined.
+        TYPE_PUBLIC_KEY_CREDENTIAL,
+        data
+      ),
+      /*isFirstUsage=*/false,
+      "tribank.us"
+    )
+  }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 6b503ff..e037db7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -36,7 +36,7 @@
           // TODO: replace to extract from the service data structure when available
           icon = context.getDrawable(R.drawable.ic_passkey)!!,
           name = it.providerId,
-          appDomainName = "tribank.us",
+          displayName = it.providerDisplayName,
           credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
           credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context)
         )
@@ -59,7 +59,8 @@
             ?: context.getDrawable(R.drawable.ic_passkey)!!,
           title = credentialEntryUi.userName.toString(),
           subtitle = credentialEntryUi.displayName?.toString() ?: "Unknown display name",
-          id = it.entryId,
+          entryKey = it.key,
+          entrySubkey = it.subkey,
           usageData = credentialEntryUi.usageData?.toString() ?: "Unknown usageData",
         )
       }
@@ -79,7 +80,7 @@
           // TODO: replace to extract from the service data structure when available
           icon = context.getDrawable(R.drawable.ic_passkey)!!,
           name = it.providerId,
-          appDomainName = "tribank.us",
+          displayName = it.providerDisplayName,
           credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
           createOptions = toCreationOptionInfoList(it.credentialEntries, context),
         )
@@ -99,7 +100,8 @@
             ?: context.getDrawable(R.drawable.ic_passkey)!!,
           title = saveEntryUi.title.toString(),
           subtitle = saveEntryUi.subTitle?.toString() ?: "Unknown subtitle",
-          id = it.entryId,
+          entryKey = it.key,
+          entrySubkey = it.subkey,
           usageData = saveEntryUi.usageData?.toString() ?: "Unknown usageData",
         )
       }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 19820d6..cb2bf10 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -21,7 +21,7 @@
 data class ProviderInfo(
   val icon: Drawable,
   val name: String,
-  val appDomainName: String,
+  val displayName: String,
   val credentialTypeIcon: Drawable,
   val createOptions: List<CreateOptionInfo>,
 )
@@ -30,10 +30,27 @@
   val icon: Drawable,
   val title: String,
   val subtitle: String,
-  val id: Int,
+  val entryKey: String,
+  val entrySubkey: String,
   val usageData: String
 )
 
+data class RequestDisplayInfo(
+  val userName: String,
+  val displayName: String,
+  val type: String,
+  val appDomainName: String,
+)
+
+/**
+ * This is initialized to be the most recent used. Can then be changed if
+ * user selects a different entry on the more option page.
+ */
+data class ActiveEntry (
+  val activeProvider: ProviderInfo,
+  val activeCreateOptionInfo: CreateOptionInfo,
+)
+
 /** The name of the current screen. */
 enum class CreateScreenState {
   PASSKEY_INTRO,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
index 82fce9f..373fb80 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyComponents.kt
@@ -39,6 +39,8 @@
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toBitmap
 import com.android.credentialmanager.R
+import com.android.credentialmanager.jetpack.CredentialEntryUi.Companion.TYPE_PASSWORD_CREDENTIAL
+import com.android.credentialmanager.jetpack.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
 import com.android.credentialmanager.ui.theme.Grey100
 import com.android.credentialmanager.ui.theme.Shapes
 import com.android.credentialmanager.ui.theme.Typography
@@ -61,30 +63,32 @@
       val uiState = viewModel.uiState
       when (uiState.currentScreenState) {
         CreateScreenState.PASSKEY_INTRO -> ConfirmationCard(
-          onConfirm = {viewModel.onConfirmIntro()},
-          onCancel = {viewModel.onCancel()},
+          onConfirm = viewModel::onConfirmIntro,
+          onCancel = viewModel::onCancel,
         )
         CreateScreenState.PROVIDER_SELECTION -> ProviderSelectionCard(
           providerList = uiState.providers,
-          onCancel = {viewModel.onCancel()},
-          onProviderSelected = {viewModel.onProviderSelected(it)}
+          onCancel = viewModel::onCancel,
+          onProviderSelected = viewModel::onProviderSelected
         )
         CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
-          providerInfo = uiState.selectedProvider!!,
-          onOptionSelected = {viewModel.onCreateOptionSelected(it)},
-          onCancel = {viewModel.onCancel()},
+          requestDisplayInfo = uiState.requestDisplayInfo,
+          providerInfo = uiState.activeEntry?.activeProvider!!,
+          createOptionInfo = uiState.activeEntry.activeCreateOptionInfo,
+          onOptionSelected = viewModel::onPrimaryCreateOptionInfoSelected,
+          onConfirm = viewModel::onPrimaryCreateOptionInfoSelected,
+          onCancel = viewModel::onCancel,
           multiProvider = uiState.providers.size > 1,
-          onMoreOptionsSelected = {viewModel.onMoreOptionsSelected(it)}
+          onMoreOptionsSelected = viewModel::onMoreOptionsSelected
         )
         CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
-            providerInfo = uiState.selectedProvider!!,
             providerList = uiState.providers,
-            onBackButtonSelected = {viewModel.onBackButtonSelected(it)},
-            onOptionSelected = {viewModel.onMoreOptionsRowSelected(it)}
+            onBackButtonSelected = viewModel::onBackButtonSelected,
+            onOptionSelected = viewModel::onMoreOptionsRowSelected
           )
         CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
-          providerInfo = uiState.selectedProvider!!,
-          onDefaultOrNotSelected = {viewModel.onDefaultOrNotSelected(it)}
+          providerInfo = uiState.activeEntry?.activeProvider!!,
+          onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
         )
       }
     },
@@ -218,10 +222,9 @@
 @ExperimentalMaterialApi
 @Composable
 fun MoreOptionsSelectionCard(
-  providerInfo: ProviderInfo,
   providerList: List<ProviderInfo>,
-  onBackButtonSelected: (String) -> Unit,
-  onOptionSelected: (String) -> Unit
+  onBackButtonSelected: () -> Unit,
+  onOptionSelected: (ActiveEntry) -> Unit
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
@@ -235,7 +238,7 @@
         elevation = 0.dp,
         navigationIcon =
         {
-          IconButton(onClick = { onBackButtonSelected(providerInfo.name) }) {
+          IconButton(onClick = onBackButtonSelected) {
             Icon(Icons.Filled.ArrowBack, "backIcon"
             )
           }
@@ -264,9 +267,12 @@
           providerList.forEach { providerInfo ->
             providerInfo.createOptions.forEach { createOptionInfo ->
               item {
-                MoreOptionsInfoRow(providerInfo = providerInfo,
+                MoreOptionsInfoRow(
+                  providerInfo = providerInfo,
                   createOptionInfo = createOptionInfo,
-                  onOptionSelected = onOptionSelected)
+                  onOptionSelected = {
+                    onOptionSelected(ActiveEntry(providerInfo, createOptionInfo))
+                  })
               }
             }
           }
@@ -285,14 +291,14 @@
 @Composable
 fun MoreOptionsRowIntroCard(
   providerInfo: ProviderInfo,
-  onDefaultOrNotSelected: (String) -> Unit,
+  onDefaultOrNotSelected: () -> Unit,
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
   ) {
     Column() {
       Text(
-        text = stringResource(R.string.use_provider_for_all_title, providerInfo.name),
+        text = stringResource(R.string.use_provider_for_all_title, providerInfo.displayName),
         style = Typography.subtitle1,
         modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
       )
@@ -302,11 +308,11 @@
       ) {
         CancelButton(
           stringResource(R.string.use_once),
-          onclick = { onDefaultOrNotSelected(providerInfo.name) }
+          onclick = onDefaultOrNotSelected
         )
         ConfirmButton(
           stringResource(R.string.set_as_default),
-          onclick = { onDefaultOrNotSelected(providerInfo.name) }
+          onclick = onDefaultOrNotSelected
         )
       }
       Divider(
@@ -338,7 +344,7 @@
     shape = Shapes.large
   ) {
     Text(
-      text = providerInfo.name,
+      text = providerInfo.displayName,
       style = Typography.button,
       modifier = Modifier.padding(vertical = 18.dp)
     )
@@ -388,11 +394,14 @@
 @ExperimentalMaterialApi
 @Composable
 fun CreationSelectionCard(
+  requestDisplayInfo: RequestDisplayInfo,
   providerInfo: ProviderInfo,
-  onOptionSelected: (Int) -> Unit,
+  createOptionInfo: CreateOptionInfo,
+  onOptionSelected: () -> Unit,
+  onConfirm: () -> Unit,
   onCancel: () -> Unit,
   multiProvider: Boolean,
-  onMoreOptionsSelected: (String) -> Unit,
+  onMoreOptionsSelected: () -> Unit,
 ) {
   Card(
     backgroundColor = lightBackgroundColor,
@@ -402,21 +411,39 @@
         bitmap = providerInfo.credentialTypeIcon.toBitmap().asImageBitmap(),
         contentDescription = null,
         tint = Color.Unspecified,
-        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(top = 24.dp)
+        modifier = Modifier.align(alignment = Alignment.CenterHorizontally).padding(all = 24.dp)
       )
       Text(
-        text = "${stringResource(R.string.choose_create_option_title)} ${providerInfo.name}",
+        text = when (requestDisplayInfo.type) {
+          TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(R.string.choose_create_option_passkey_title,
+            providerInfo.displayName)
+          TYPE_PASSWORD_CREDENTIAL -> stringResource(R.string.choose_create_option_password_title,
+            providerInfo.displayName)
+          else -> stringResource(R.string.choose_create_option_sign_in_title,
+            providerInfo.displayName)
+        },
         style = Typography.subtitle1,
-        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
+        modifier = Modifier.padding(horizontal = 24.dp)
+          .align(alignment = Alignment.CenterHorizontally),
+        textAlign = TextAlign.Center,
       )
       Text(
-        text = providerInfo.appDomainName,
+        text = requestDisplayInfo.appDomainName,
         style = Typography.body2,
-        modifier = Modifier.padding(horizontal = 28.dp)
+        modifier = Modifier.align(alignment = Alignment.CenterHorizontally)
       )
-      Divider(
-        thickness = 24.dp,
-        color = Color.Transparent
+      Text(
+        text = stringResource(
+          R.string.choose_create_option_description,
+          when (requestDisplayInfo.type) {
+            TYPE_PUBLIC_KEY_CREDENTIAL -> "passkeys"
+            TYPE_PASSWORD_CREDENTIAL -> "passwords"
+            else -> "sign-ins"
+          },
+          providerInfo.displayName,
+          createOptionInfo.title),
+        style = Typography.body1,
+        modifier = Modifier.padding(all = 24.dp).align(alignment = Alignment.CenterHorizontally)
       )
       Card(
         shape = Shapes.medium,
@@ -427,14 +454,13 @@
         LazyColumn(
           verticalArrangement = Arrangement.spacedBy(2.dp)
         ) {
-          providerInfo.createOptions.forEach {
             item {
-              CreateOptionRow(createOptionInfo = it, onOptionSelected = onOptionSelected)
+              PrimaryCreateOptionRow(requestDisplayInfo = requestDisplayInfo,
+                onOptionSelected = onOptionSelected)
             }
-          }
           if (multiProvider) {
             item {
-              MoreOptionsRow(onSelect = { onMoreOptionsSelected(providerInfo.name) })
+              MoreOptionsRow(onSelect = onMoreOptionsSelected)
             }
           }
         }
@@ -444,10 +470,17 @@
         color = Color.Transparent
       )
       Row(
-        horizontalArrangement = Arrangement.Start,
+        horizontalArrangement = Arrangement.SpaceBetween,
         modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
       ) {
-        CancelButton(stringResource(R.string.string_cancel), onCancel)
+        CancelButton(
+          stringResource(R.string.string_cancel),
+          onclick = onCancel
+        )
+        ConfirmButton(
+          stringResource(R.string.string_continue),
+          onclick = onConfirm
+        )
       }
       Divider(
         thickness = 18.dp,
@@ -460,17 +493,14 @@
 
 @ExperimentalMaterialApi
 @Composable
-fun CreateOptionRow(createOptionInfo: CreateOptionInfo, onOptionSelected: (Int) -> Unit) {
+fun PrimaryCreateOptionRow(
+  requestDisplayInfo: RequestDisplayInfo,
+  onOptionSelected: () -> Unit
+) {
   Chip(
     modifier = Modifier.fillMaxWidth(),
-    onClick = {onOptionSelected(createOptionInfo.id)},
-    leadingIcon = {
-      Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
-        bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(),
-            // painter = painterResource(R.drawable.ic_passkey),
-        // TODO: add description.
-            contentDescription = "")
-    },
+    onClick = onOptionSelected,
+    // TODO: Add an icon generated by provider according to requestDisplayInfo type
     colors = ChipDefaults.chipColors(
       backgroundColor = Grey100,
       leadingIconContentColor = Grey100
@@ -479,12 +509,12 @@
   ) {
     Column() {
       Text(
-        text = createOptionInfo.title,
+        text = requestDisplayInfo.userName,
         style = Typography.h6,
         modifier = Modifier.padding(top = 16.dp)
       )
       Text(
-        text = createOptionInfo.subtitle,
+        text = requestDisplayInfo.displayName,
         style = Typography.body2,
         modifier = Modifier.padding(bottom = 16.dp)
       )
@@ -497,11 +527,11 @@
 fun MoreOptionsInfoRow(
   providerInfo: ProviderInfo,
   createOptionInfo: CreateOptionInfo,
-  onOptionSelected: (String) -> Unit
+  onOptionSelected: () -> Unit
 ) {
     Chip(
         modifier = Modifier.fillMaxWidth(),
-        onClick = { onOptionSelected(providerInfo.name) },
+        onClick = onOptionSelected,
         leadingIcon = {
             Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
                 bitmap = createOptionInfo.icon.toBitmap().asImageBitmap(),
@@ -517,8 +547,12 @@
     ) {
         Column() {
             Text(
-                text = if (providerInfo.createOptions.size > 1)
-                {providerInfo.name + " for " + createOptionInfo.title} else { providerInfo.name},
+                text =
+                if (providerInfo.createOptions.size > 1)
+                {stringResource(R.string.more_options_title_multiple_options,
+                  providerInfo.displayName, createOptionInfo.title)} else {
+                  stringResource(R.string.more_options_title_one_option,
+                    providerInfo.displayName)},
                 style = Typography.h6,
                 modifier = Modifier.padding(top = 16.dp)
             )
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
index ff44e2e..615da4e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreatePasskeyViewModel.kt
@@ -30,7 +30,8 @@
 data class CreatePasskeyUiState(
   val providers: List<ProviderInfo>,
   val currentScreenState: CreateScreenState,
-  val selectedProvider: ProviderInfo? = null,
+  val requestDisplayInfo: RequestDisplayInfo,
+  val activeEntry: ActiveEntry? = null,
 )
 
 class CreatePasskeyViewModel(
@@ -56,7 +57,8 @@
     } else if (uiState.providers.size == 1){
       uiState = uiState.copy(
         currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        selectedProvider = uiState.providers.first()
+        activeEntry = ActiveEntry(uiState.providers.first(),
+          uiState.providers.first().createOptions.first())
       )
     } else {
       throw java.lang.IllegalStateException("Empty provider list.")
@@ -66,15 +68,20 @@
   fun onProviderSelected(providerName: String) {
     uiState = uiState.copy(
       currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-      selectedProvider = getProviderInfoByName(providerName)
+      activeEntry = ActiveEntry(getProviderInfoByName(providerName),
+        getProviderInfoByName(providerName).createOptions.first())
     )
   }
 
-  fun onCreateOptionSelected(createOptionId: Int) {
-    Log.d("Account Selector", "Option selected for creation: $createOptionId")
+  fun onCreateOptionSelected(entryKey: String, entrySubkey: String) {
+    Log.d(
+      "Account Selector",
+      "Option selected for creation: {key = $entryKey, subkey = $entrySubkey}"
+    )
     CredentialManagerRepo.getInstance().onOptionSelected(
-      uiState.selectedProvider!!.name,
-      createOptionId
+      uiState.activeEntry?.activeProvider!!.name,
+      entryKey,
+      entrySubkey
     )
     dialogResult.value = DialogResult(
       ResultState.COMPLETE,
@@ -87,24 +94,22 @@
     }
   }
 
-  fun onMoreOptionsSelected(providerName: String) {
+  fun onMoreOptionsSelected() {
     uiState = uiState.copy(
-        currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
-        selectedProvider = getProviderInfoByName(providerName)
+      currentScreenState = CreateScreenState.MORE_OPTIONS_SELECTION,
     )
   }
 
-  fun onBackButtonSelected(providerName: String) {
+  fun onBackButtonSelected() {
     uiState = uiState.copy(
         currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-        selectedProvider = getProviderInfoByName(providerName)
     )
   }
 
-  fun onMoreOptionsRowSelected(providerName: String) {
+  fun onMoreOptionsRowSelected(activeEntry: ActiveEntry) {
     uiState = uiState.copy(
       currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO,
-      selectedProvider = getProviderInfoByName(providerName)
+      activeEntry = activeEntry
     )
   }
 
@@ -113,11 +118,32 @@
     dialogResult.value = DialogResult(ResultState.CANCELED)
   }
 
-  fun onDefaultOrNotSelected(providerName: String) {
+  fun onDefaultOrNotSelected() {
     uiState = uiState.copy(
       currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
-      selectedProvider = getProviderInfoByName(providerName)
     )
     // TODO: implement the if choose as default or not logic later
   }
+
+  fun onPrimaryCreateOptionInfoSelected() {
+    var createOptionEntryKey = uiState.activeEntry?.activeCreateOptionInfo?.entryKey
+    var createOptionEntrySubkey = uiState.activeEntry?.activeCreateOptionInfo?.entrySubkey
+    Log.d(
+      "Account Selector",
+      "Option selected for creation: " +
+              "{key = $createOptionEntryKey, subkey = $createOptionEntrySubkey}"
+    )
+    if (createOptionEntryKey != null && createOptionEntrySubkey != null) {
+      CredentialManagerRepo.getInstance().onOptionSelected(
+        uiState.activeEntry?.activeProvider!!.name,
+        createOptionEntryKey,
+        createOptionEntrySubkey
+      )
+    } else {
+      TODO("Gracefully handle illegal state.")
+    }
+    dialogResult.value = DialogResult(
+      ResultState.COMPLETE,
+    )
+  }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index 48c67bb..e3398c0 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -66,11 +66,12 @@
       val uiState = viewModel.uiState
       when (uiState.currentScreenState) {
         GetScreenState.CREDENTIAL_SELECTION -> CredentialSelectionCard(
+          requestDisplayInfo = uiState.requestDisplayInfo,
           providerInfo = uiState.selectedProvider!!,
-          onCancel = {viewModel.onCancel()},
-          onOptionSelected = {viewModel.onCredentailSelected(it)},
+          onCancel = viewModel::onCancel,
+          onOptionSelected = viewModel::onCredentailSelected,
           multiProvider = uiState.providers.size > 1,
-          onMoreOptionSelected = {viewModel.onMoreOptionSelected()},
+          onMoreOptionSelected = viewModel::onMoreOptionSelected,
         )
       }
     },
@@ -87,8 +88,9 @@
 @ExperimentalMaterialApi
 @Composable
 fun CredentialSelectionCard(
+  requestDisplayInfo: RequestDisplayInfo,
   providerInfo: ProviderInfo,
-  onOptionSelected: (Int) -> Unit,
+  onOptionSelected: (String, String) -> Unit,
   onCancel: () -> Unit,
   multiProvider: Boolean,
   onMoreOptionSelected: () -> Unit,
@@ -111,7 +113,7 @@
           .align(alignment = Alignment.CenterHorizontally)
       )
       Text(
-        text = providerInfo.appDomainName,
+        text = requestDisplayInfo.appDomainName,
         style = Typography.body2,
         modifier = Modifier.padding(horizontal = 28.dp)
       )
@@ -163,11 +165,11 @@
 @Composable
 fun CredentialOptionRow(
     credentialOptionInfo: CredentialOptionInfo,
-    onOptionSelected: (Int) -> Unit
+    onOptionSelected: (String, String) -> Unit,
 ) {
   Chip(
     modifier = Modifier.fillMaxWidth(),
-    onClick = {onOptionSelected(credentialOptionInfo.id)},
+    onClick = {onOptionSelected(credentialOptionInfo.entryKey, credentialOptionInfo.entrySubkey)},
     leadingIcon = {
       Image(modifier = Modifier.size(24.dp, 24.dp).padding(start = 10.dp),
             bitmap = credentialOptionInfo.icon.toBitmap().asImageBitmap(),
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 33858f5..7b6c30a 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -30,6 +30,7 @@
 data class GetCredentialUiState(
   val providers: List<ProviderInfo>,
   val currentScreenState: GetScreenState,
+  val requestDisplayInfo: RequestDisplayInfo,
   val selectedProvider: ProviderInfo? = null,
 )
 
@@ -48,11 +49,12 @@
     return dialogResult
   }
 
-  fun onCredentailSelected(credentialId: Int) {
-    Log.d("Account Selector", "credential selected: $credentialId")
+  fun onCredentailSelected(entryKey: String, entrySubkey: String) {
+    Log.d("Account Selector", "credential selected: {key=$entryKey,subkey=$entrySubkey}")
     CredentialManagerRepo.getInstance().onOptionSelected(
       uiState.selectedProvider!!.name,
-      credentialId
+      entryKey,
+      entrySubkey
     )
     dialogResult.value = DialogResult(
       ResultState.COMPLETE,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index a39b211..b6ecd37 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -21,7 +21,7 @@
 data class ProviderInfo(
   val icon: Drawable,
   val name: String,
-  val appDomainName: String,
+  val displayName: String,
   val credentialTypeIcon: Drawable,
   val credentialOptions: List<CredentialOptionInfo>,
 )
@@ -30,10 +30,18 @@
   val icon: Drawable,
   val title: String,
   val subtitle: String,
-  val id: Int,
+  val entryKey: String,
+  val entrySubkey: String,
   val usageData: String
 )
 
+data class RequestDisplayInfo(
+  val userName: String,
+  val displayName: String,
+  val type: String,
+  val appDomainName: String,
+)
+
 /** The name of the current screen. */
 enum class GetScreenState {
   CREDENTIAL_SELECTION,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt
new file mode 100644
index 0000000..d6f1b5f
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/CredentialEntryUi.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+abstract class CredentialEntryUi(
+  val credentialTypeIcon: Icon,
+  val profileIcon: Icon?,
+  val lastUsedTimeMillis: Long?,
+  val note: CharSequence?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      return when (slice.spec?.type) {
+        TYPE_PUBLIC_KEY_CREDENTIAL -> PasskeyCredentialEntryUi.fromSlice(slice)
+        TYPE_PASSWORD_CREDENTIAL -> PasswordCredentialEntryUi.fromSlice(slice)
+        else -> throw IllegalArgumentException("Unexpected type: ${slice.spec?.type}")
+      }
+    }
+
+    const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
+      "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+    const val TYPE_PASSWORD_CREDENTIAL: String = "androidx.credentials.TYPE_PASSWORD"
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt
new file mode 100644
index 0000000..bb3b206
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasskeyCredentialEntryUi.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+class PasskeyCredentialEntryUi(
+  val userName: CharSequence,
+  val userDisplayName: CharSequence?,
+  credentialTypeIcon: Icon,
+  profileIcon: Icon?,
+  lastUsedTimeMillis: Long?,
+  note: CharSequence?,
+) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      var userName: CharSequence? = null
+      var userDisplayName: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var lastUsedTimeMillis: Long? = null
+      var note: CharSequence? = null
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_NAME)) {
+          userName = it.text
+        } else if (it.hasHint(Entry.HINT_PASSKEY_USER_DISPLAY_NAME)) {
+          userDisplayName = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        } else if (it.hasHint(Entry.HINT_NOTE)) {
+          note = it.text
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return PasskeyCredentialEntryUi(
+        userName!!, userDisplayName, credentialTypeIcon!!,
+        profileIcon, lastUsedTimeMillis, note,
+      )
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt
new file mode 100644
index 0000000..7311b70
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/PasswordCredentialEntryUi.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a password credential entry used during the get credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class PasswordCredentialEntryUi(
+  val userName: CharSequence,
+  val password: CharSequence,
+  credentialTypeIcon: Icon,
+  profileIcon: Icon?,
+  lastUsedTimeMillis: Long?,
+  note: CharSequence?,
+) : CredentialEntryUi(credentialTypeIcon, profileIcon, lastUsedTimeMillis, note) {
+  companion object {
+    fun fromSlice(slice: Slice): CredentialEntryUi {
+      var userName: CharSequence? = null
+      var password: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var lastUsedTimeMillis: Long? = null
+      var note: CharSequence? = null
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_NAME)) {
+          userName = it.text
+        } else if (it.hasHint(Entry.HINT_PASSWORD_VALUE)) {
+          password = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        } else if (it.hasHint(Entry.HINT_NOTE)) {
+          note = it.text
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return PasswordCredentialEntryUi(
+        userName!!, password!!, credentialTypeIcon!!,
+        profileIcon, lastUsedTimeMillis, note,
+      )
+    }
+  }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt
new file mode 100644
index 0000000..fad3309
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/SaveEntryUi.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.jetpack
+
+import android.app.slice.Slice
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+
+/**
+ * UI representation for a save entry used during the create credential flow.
+ *
+ * TODO: move to jetpack.
+ */
+class SaveEntryUi(
+  val userProviderAccountName: CharSequence,
+  val credentialTypeIcon: Icon?,
+  val profileIcon: Icon?,
+  val passwordCount: Int?,
+  val passkeyCount: Int?,
+  val totalCredentialCount: Int?,
+  val lastUsedTimeMillis: Long?,
+) {
+  companion object {
+    fun fromSlice(slice: Slice): SaveEntryUi {
+      var userProviderAccountName: CharSequence? = null
+      var credentialTypeIcon: Icon? = null
+      var profileIcon: Icon? = null
+      var passwordCount: Int? = null
+      var passkeyCount: Int? = null
+      var totalCredentialCount: Int? = null
+      var lastUsedTimeMillis: Long? = null
+
+
+      val items = slice.items
+      items.forEach {
+        if (it.hasHint(Entry.HINT_USER_PROVIDER_ACCOUNT_NAME)) {
+          userProviderAccountName = it.text
+        } else if (it.hasHint(Entry.HINT_CREDENTIAL_TYPE_ICON)) {
+          credentialTypeIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PROFILE_ICON)) {
+          profileIcon = it.icon
+        } else if (it.hasHint(Entry.HINT_PASSWORD_COUNT)) {
+          passwordCount = it.int
+        } else if (it.hasHint(Entry.HINT_PASSKEY_COUNT)) {
+          passkeyCount = it.int
+        } else if (it.hasHint(Entry.HINT_TOTAL_CREDENTIAL_COUNT)) {
+          totalCredentialCount = it.int
+        } else if (it.hasHint(Entry.HINT_LAST_USED_TIME_MILLIS)) {
+          lastUsedTimeMillis = it.long
+        }
+      }
+      // TODO: fail NPE more elegantly.
+      return SaveEntryUi(
+        userProviderAccountName!!, credentialTypeIcon, profileIcon,
+        passwordCount, passkeyCount, totalCredentialCount, lastUsedTimeMillis,
+      )
+    }
+  }
+}
diff --git a/packages/EasterEgg/Android.bp b/packages/EasterEgg/Android.bp
index f8785f2..e88410c 100644
--- a/packages/EasterEgg/Android.bp
+++ b/packages/EasterEgg/Android.bp
@@ -36,7 +36,7 @@
     certificate: "platform",
 
     optimize: {
-        enabled: false,
+        proguard_flags_files: ["proguard.flags"],
     },
 
 	static_libs: [
diff --git a/packages/EasterEgg/proguard.flags b/packages/EasterEgg/proguard.flags
new file mode 100644
index 0000000..b333ab0
--- /dev/null
+++ b/packages/EasterEgg/proguard.flags
@@ -0,0 +1,4 @@
+# Note: This is a very conservative keep rule, but as the amount of app
+# code is small, this minimizes any maintenance risks while providing
+# most of the shrinking benefits for referenced libraries.
+-keep class com.android.egg.** { *; }
diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp
index 332bebf..c35fb3b 100644
--- a/packages/SettingsLib/ActivityEmbedding/Android.bp
+++ b/packages/SettingsLib/ActivityEmbedding/Android.bp
@@ -26,4 +26,9 @@
         "androidx.window.extensions",
         "androidx.window.sidecar",
     ],
+
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.permission",
+    ],
 }
diff --git a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
index 2742558..0949e1d 100644
--- a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
+++ b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
@@ -21,6 +21,7 @@
     <uses-sdk android:minSdkVersion="21" />
 
     <application>
+        <uses-library android:name="org.apache.http.legacy" android:required="false" />
         <uses-library android:name="androidx.window.extensions" android:required="false" />
         <uses-library android:name="androidx.window.sidecar" android:required="false" />
     </application>
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 68c63da..4fb77d7 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -18,9 +18,8 @@
     ext {
         spa_min_sdk = 21
         spa_target_sdk = 33
-        jetpack_compose_version = '1.2.0-alpha04'
+        jetpack_compose_version = '1.3.0'
         jetpack_compose_compiler_version = '1.3.2'
-        jetpack_compose_material3_version = '1.0.0-alpha06'
     }
 }
 plugins {
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index c1ce7d9..20dd707 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -54,11 +54,6 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
-    packagingOptions {
-        resources {
-            excludes += '/META-INF/{AL2.0,LGPL2.1}'
-        }
-    }
 }
 
 dependencies {
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index acb22da..4af2589 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -25,6 +25,7 @@
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
 import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
 import com.android.settingslib.spa.gallery.page.SliderPageProvider
 import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider
@@ -66,6 +67,7 @@
                 IllustrationPageProvider,
                 CategoryPageProvider,
                 ActionButtonPageProvider,
+                ProgressBarPageProvider,
             ),
             rootPages = listOf(
                 HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
index e40775a..7fd49db 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt
@@ -31,6 +31,7 @@
 import com.android.settingslib.spa.gallery.page.ArgumentPageProvider
 import com.android.settingslib.spa.gallery.page.FooterPageProvider
 import com.android.settingslib.spa.gallery.page.IllustrationPageProvider
+import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
 import com.android.settingslib.spa.gallery.page.SettingsPagerPageProvider
 import com.android.settingslib.spa.gallery.page.SliderPageProvider
 import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
@@ -54,6 +55,7 @@
             IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            ProgressBarPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
         )
     }
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
new file mode 100644
index 0000000..dc45df4
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ProgressBarPage.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.SystemUpdate
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.ProgressBarPreference
+import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel
+import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CircularLoadingBar
+import com.android.settingslib.spa.widget.ui.CircularProgressBar
+import com.android.settingslib.spa.widget.ui.LinearLoadingBar
+import kotlinx.coroutines.delay
+
+private const val TITLE = "Sample ProgressBar"
+
+object ProgressBarPageProvider : SettingsPageProvider {
+    override val name = "ProgressBar"
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name))
+            .setIsAllowSearch(true)
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        // Mocks a loading time of 2 seconds.
+        var loading by remember { mutableStateOf(true) }
+        LaunchedEffect(Unit) {
+            delay(2000)
+            loading = false
+        }
+
+        RegularScaffold(title = TITLE) {
+            // Auto update the progress and finally jump tp 0.4f.
+            var progress by remember { mutableStateOf(0f) }
+            LaunchedEffect(Unit) {
+                delay(2000)
+                while (progress < 1f) {
+                    delay(100)
+                    progress += 0.01f
+                }
+                delay(500)
+                progress = 0.4f
+            }
+
+            // Show as a placeholder for progress bar
+            LargeProgressBar(progress)
+            // The remaining information only shows after loading complete.
+            if (!loading) {
+                SimpleProgressBar()
+                ProgressBarWithData()
+                CircularProgressBar(progress = progress, radius = 160f)
+            }
+        }
+
+        // Add loading bar examples, running for 2 seconds.
+        LinearLoadingBar(isLoading = loading, yOffset = 64.dp)
+        CircularLoadingBar(isLoading = loading)
+    }
+}
+
+@Composable
+private fun LargeProgressBar(progress: Float) {
+    ProgressBarPreference(object : ProgressBarPreferenceModel {
+        override val title = "Large Progress Bar"
+        override val progress = progress
+        override val height = 20f
+    })
+}
+
+@Composable
+private fun SimpleProgressBar() {
+    ProgressBarPreference(object : ProgressBarPreferenceModel {
+        override val title = "Simple Progress Bar"
+        override val progress = 0.2f
+        override val icon = Icons.Outlined.SystemUpdate
+    })
+}
+
+@Composable
+private fun ProgressBarWithData() {
+    ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel {
+        override val title = "Progress Bar with Data"
+        override val progress = 0.2f
+        override val icon = Icons.Outlined.Delete
+    }, data = "25G")
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ProgressBarPagePreview() {
+    SettingsTheme {
+        ProgressBarPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
index e09ebda..b38178b 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -17,7 +17,10 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPage
@@ -48,9 +51,11 @@
 
     @Composable
     override fun Page(arguments: Bundle?) {
-        SettingsScaffold(title = TITLE) {
-            SettingsPager(listOf("Personal", "Work")) {
-                PlaceholderTitle("Page $it")
+        SettingsScaffold(title = TITLE) { paddingValues ->
+            Box(Modifier.padding(paddingValues)) {
+                SettingsPager(listOf("Personal", "Work")) {
+                    PlaceholderTitle("Page $it")
+                }
             }
         }
     }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
index a2a913f..fa8d51c 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.common.SettingsEntry
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
@@ -104,6 +105,7 @@
         entryList.add(
             createEntry(EntryEnum.DISABLED_PREFERENCE)
                 .setIsAllowSearch(true)
+                .setHasMutableStatus(true)
                 .setMacro {
                     spaLogger.message(TAG, "create macro for ${EntryEnum.DISABLED_PREFERENCE}")
                     SimplePreferenceMacro(
@@ -113,14 +115,17 @@
                         icon = Icons.Outlined.DisabledByDefault,
                     )
                 }
+                .setStatusDataFn { EntryStatusData(isDisabled = true) }
                 .build()
         )
         entryList.add(
             createEntry(EntryEnum.ASYNC_SUMMARY_PREFERENCE)
                 .setIsAllowSearch(true)
+                .setHasMutableStatus(true)
                 .setSearchDataFn {
                     EntrySearchData(title = ASYNC_PREFERENCE_TITLE)
                 }
+                .setStatusDataFn { EntryStatusData(isDisabled = false) }
                 .setUiLayoutFn {
                     val model = PreferencePageModel.create()
                     val asyncSummary = remember { model.getAsyncSummary() }
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index c587411..2820ed7 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -51,16 +51,11 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
-    packagingOptions {
-        resources {
-            excludes += '/META-INF/{AL2.0,LGPL2.1}'
-        }
-    }
 }
 
 dependencies {
     api "androidx.appcompat:appcompat:1.7.0-alpha01"
-    api "androidx.compose.material3:material3:$jetpack_compose_material3_version"
+    api "androidx.compose.material3:material3:1.1.0-alpha01"
     api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
     api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
     api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index 89daeb1..d3efaa7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -35,6 +35,7 @@
 import androidx.navigation.compose.rememberNavController
 import com.android.settingslib.spa.R
 import com.android.settingslib.spa.framework.common.LogCategory
+import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.framework.compose.LocalNavController
@@ -44,7 +45,6 @@
 import com.android.settingslib.spa.framework.util.navRoute
 
 private const val TAG = "BrowseActivity"
-private const val NULL_PAGE_NAME = "NULL"
 
 /**
  * The Activity to render ALL SPA pages, and handles jumps between SPA pages.
@@ -81,9 +81,10 @@
     private fun MainContent() {
         val sppRepository by spaEnvironment.pageProviderRepository
         val navController = rememberNavController()
+        val nullPage = SettingsPage.createNull()
         CompositionLocalProvider(navController.localNavController()) {
-            NavHost(navController, NULL_PAGE_NAME) {
-                composable(NULL_PAGE_NAME) {}
+            NavHost(navController, nullPage.sppName) {
+                composable(nullPage.sppName) {}
                 for (spp in sppRepository.getAllProviders()) {
                     composable(
                         route = spp.name + spp.parameter.navRoute(),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
index d631708..38f41bc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/EntryProvider.kt
@@ -42,9 +42,10 @@
  * For gallery, AuthorityPath = com.android.spa.gallery.provider
  * For Settings, AuthorityPath = com.android.settings.spa.provider
  * Some examples:
- *   $ adb shell content query --uri content://<AuthorityPath>/search_sitemap
  *   $ adb shell content query --uri content://<AuthorityPath>/search_static
  *   $ adb shell content query --uri content://<AuthorityPath>/search_dynamic
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_mutable_status
+ *   $ adb shell content query --uri content://<AuthorityPath>/search_immutable_status
  */
 open class EntryProvider : ContentProvider() {
     private val spaEnvironment get() = SpaEnvironmentFactory.instance
@@ -81,9 +82,10 @@
 
     override fun attachInfo(context: Context?, info: ProviderInfo?) {
         if (info != null) {
-            QueryEnum.SEARCH_SITEMAP_QUERY.addUri(uriMatcher, info.authority)
             QueryEnum.SEARCH_STATIC_DATA_QUERY.addUri(uriMatcher, info.authority)
             QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
+            QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.addUri(uriMatcher, info.authority)
         }
         super.attachInfo(context, info)
     }
@@ -97,9 +99,12 @@
     ): Cursor? {
         return try {
             when (uriMatcher.match(uri)) {
-                QueryEnum.SEARCH_SITEMAP_QUERY.queryMatchCode -> querySearchSitemap()
                 QueryEnum.SEARCH_STATIC_DATA_QUERY.queryMatchCode -> querySearchStaticData()
                 QueryEnum.SEARCH_DYNAMIC_DATA_QUERY.queryMatchCode -> querySearchDynamicData()
+                QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+                    querySearchMutableStatusData()
+                QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.queryMatchCode ->
+                    querySearchImmutableStatusData()
                 else -> throw UnsupportedOperationException("Unknown Uri $uri")
             }
         } catch (e: UnsupportedOperationException) {
@@ -110,18 +115,22 @@
         }
     }
 
-    private fun querySearchSitemap(): Cursor {
+    private fun querySearchImmutableStatusData(): Cursor {
         val entryRepository by spaEnvironment.entryRepository
-        val cursor = MatrixCursor(QueryEnum.SEARCH_SITEMAP_QUERY.getColumns())
+        val cursor = MatrixCursor(QueryEnum.SEARCH_IMMUTABLE_STATUS_DATA_QUERY.getColumns())
         for (entry in entryRepository.getAllEntries()) {
-            if (!entry.isAllowSearch) continue
-            val intent = entry.containerPage()
-                .createBrowseIntent(context, spaEnvironment.browseActivityClass, entry.id)
-                ?: Intent()
-            cursor.newRow()
-                .add(ColumnEnum.ENTRY_ID.id, entry.id)
-                .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id, entryRepository.getEntryPath(entry.id))
-                .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
+            if (!entry.isAllowSearch || entry.mutableStatus) continue
+            fetchStatusData(entry, cursor)
+        }
+        return cursor
+    }
+
+    private fun querySearchMutableStatusData(): Cursor {
+        val entryRepository by spaEnvironment.entryRepository
+        val cursor = MatrixCursor(QueryEnum.SEARCH_MUTABLE_STATUS_DATA_QUERY.getColumns())
+        for (entry in entryRepository.getAllEntries()) {
+            if (!entry.isAllowSearch || !entry.mutableStatus) continue
+            fetchStatusData(entry, cursor)
         }
         return cursor
     }
@@ -147,14 +156,28 @@
     }
 
     private fun fetchSearchData(entry: SettingsEntry, cursor: MatrixCursor) {
+        val entryRepository by spaEnvironment.entryRepository
+        val browseActivityClass = spaEnvironment.browseActivityClass
+
         // Fetch search data. We can add runtime arguments later if necessary
-        val searchData = entry.getSearchData()
+        val searchData = entry.getSearchData() ?: return
+        val intent = entry.containerPage()
+            .createBrowseIntent(context, browseActivityClass, entry.id)
+            ?: Intent()
         cursor.newRow()
             .add(ColumnEnum.ENTRY_ID.id, entry.id)
-            .add(ColumnEnum.ENTRY_TITLE.id, searchData?.title ?: "")
-            .add(
-                ColumnEnum.ENTRY_SEARCH_KEYWORD.id,
-                searchData?.keyword ?: emptyList<String>()
-            )
+            .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(Intent.URI_INTENT_SCHEME))
+            .add(ColumnEnum.SEARCH_TITLE.id, searchData.title)
+            .add(ColumnEnum.SEARCH_KEYWORD.id, searchData.keyword)
+            .add(ColumnEnum.SEARCH_PATH.id,
+                entryRepository.getEntryPathWithTitle(entry.id, searchData.title))
+    }
+
+    private fun fetchStatusData(entry: SettingsEntry, cursor: MatrixCursor) {
+        // Fetch status data. We can add runtime arguments later if necessary
+        val statusData = entry.getStatusData() ?: return
+        cursor.newRow()
+            .add(ColumnEnum.ENTRY_ID.id, entry.id)
+            .add(ColumnEnum.SEARCH_STATUS_DISABLED.id, statusData.isDisabled)
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt
index 9ec0c01..b3571a1 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryMacro.kt
@@ -26,4 +26,5 @@
     @Composable
     fun UiLayout() {}
     fun getSearchData(): EntrySearchData? = null
+    fun getStatusData(): EntryStatusData? = null
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt
index 9b262af..9bc620f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntrySearchData.kt
@@ -22,12 +22,4 @@
 data class EntrySearchData(
     val title: String = "",
     val keyword: List<String> = emptyList(),
-) {
-    fun format(): String {
-        val content = listOf(
-            "search_title = $title",
-            "search_keyword = $keyword",
-        )
-        return content.joinToString("\n")
-    }
-}
+)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryStatusData.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryStatusData.kt
new file mode 100644
index 0000000..3e9dd3b
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/EntryStatusData.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.common
+
+/**
+ * Defines the status data of one Settings entry, which could be changed frequently.
+ */
+data class EntryStatusData(
+    val isDisabled: Boolean = false,
+    val isSwitchOff: Boolean = false,
+)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
index 0707429..121c07f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/ProviderColumn.kt
@@ -40,8 +40,10 @@
     ENTRY_START_ADB("entryStartAdb"),
 
     // Columns related to search
-    ENTRY_TITLE("entryTitle"),
-    ENTRY_SEARCH_KEYWORD("entrySearchKw"),
+    SEARCH_TITLE("searchTitle"),
+    SEARCH_KEYWORD("searchKw"),
+    SEARCH_PATH("searchPath"),
+    SEARCH_STATUS_DISABLED("searchDisabled"),
 }
 
 /**
@@ -83,32 +85,42 @@
             ColumnEnum.ENTRY_NAME,
             ColumnEnum.ENTRY_ROUTE,
             ColumnEnum.ENTRY_INTENT_URI,
+            ColumnEnum.ENTRY_HIERARCHY_PATH,
         )
     ),
 
-    // Search related queries
-    SEARCH_SITEMAP_QUERY(
-        "search_sitemap", 300,
-        listOf(
-            ColumnEnum.ENTRY_ID,
-            ColumnEnum.ENTRY_HIERARCHY_PATH,
-            ColumnEnum.ENTRY_INTENT_URI,
-        )
-    ),
     SEARCH_STATIC_DATA_QUERY(
         "search_static", 301,
         listOf(
             ColumnEnum.ENTRY_ID,
-            ColumnEnum.ENTRY_TITLE,
-            ColumnEnum.ENTRY_SEARCH_KEYWORD,
+            ColumnEnum.ENTRY_INTENT_URI,
+            ColumnEnum.SEARCH_TITLE,
+            ColumnEnum.SEARCH_KEYWORD,
+            ColumnEnum.SEARCH_PATH,
         )
     ),
     SEARCH_DYNAMIC_DATA_QUERY(
         "search_dynamic", 302,
         listOf(
             ColumnEnum.ENTRY_ID,
-            ColumnEnum.ENTRY_TITLE,
-            ColumnEnum.ENTRY_SEARCH_KEYWORD,
+            ColumnEnum.ENTRY_INTENT_URI,
+            ColumnEnum.SEARCH_TITLE,
+            ColumnEnum.SEARCH_KEYWORD,
+            ColumnEnum.SEARCH_PATH,
+        )
+    ),
+    SEARCH_IMMUTABLE_STATUS_DATA_QUERY(
+        "search_immutable_status", 303,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.SEARCH_STATUS_DISABLED,
+        )
+    ),
+    SEARCH_MUTABLE_STATUS_DATA_QUERY(
+        "search_mutable_status", 304,
+        listOf(
+            ColumnEnum.ENTRY_ID,
+            ColumnEnum.SEARCH_STATUS_DISABLED,
         )
     ),
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 8616b9f..224fe1d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -37,7 +37,7 @@
 }
 
 val LocalEntryDataProvider =
-    compositionLocalOf<EntryData> { object : EntryData{} }
+    compositionLocalOf<EntryData> { object : EntryData {} }
 
 /**
  * Defines data of a Settings entry.
@@ -65,8 +65,14 @@
      * ========================================
      */
     val isAllowSearch: Boolean = false,
+
+    // Indicate whether the search indexing data of entry is dynamic.
     val isSearchDataDynamic: Boolean = false,
 
+    // Indicate whether the status of entry is mutable.
+    // If so, for instance, we'll reindex its status for search.
+    val mutableStatus: Boolean = false,
+
     /**
      * ========================================
      * Defines entry APIs to get data here.
@@ -74,8 +80,14 @@
      */
 
     /**
-     * API to get Search related data for this entry.
-     * Returns null if this entry is not available for the search at the moment.
+     * API to get the status data of the entry, such as isDisabled / isSwitchOff.
+     * Returns null if this entry do NOT have any status.
+     */
+    private val statusDataImpl: (arguments: Bundle?) -> EntryStatusData? = { null },
+
+    /**
+     * API to get Search indexing data for this entry, such as title / keyword.
+     * Returns null if this entry do NOT support search.
      */
     private val searchDataImpl: (arguments: Bundle?) -> EntrySearchData? = { null },
 
@@ -87,21 +99,6 @@
      */
     private val uiLayoutImpl: (@Composable (arguments: Bundle?) -> Unit) = {},
 ) {
-    fun formatContent(): String {
-        val content = listOf(
-            "id = $id",
-            "owner = ${owner.formatDisplayTitle()}",
-            "linkFrom = ${fromPage?.formatDisplayTitle()}",
-            "linkTo = ${toPage?.formatDisplayTitle()}",
-            "${getSearchData()?.format()}",
-        )
-        return content.joinToString("\n")
-    }
-
-    fun displayTitle(): String {
-        return "${owner.displayName}:$displayName"
-    }
-
     fun containerPage(): SettingsPage {
         // The Container page of the entry, which is the from-page or
         // the owner-page if from-page is unset.
@@ -116,6 +113,10 @@
         return arguments
     }
 
+    fun getStatusData(runtimeArguments: Bundle? = null): EntryStatusData? {
+        return statusDataImpl(fullArgument(runtimeArguments))
+    }
+
     fun getSearchData(runtimeArguments: Bundle? = null): EntrySearchData? {
         return searchDataImpl(fullArgument(runtimeArguments))
     }
@@ -151,8 +152,10 @@
     // Attributes
     private var isAllowSearch: Boolean = false
     private var isSearchDataDynamic: Boolean = false
+    private var mutableStatus: Boolean = false
 
     // Functions
+    private var statusDataFn: (arguments: Bundle?) -> EntryStatusData? = { null }
     private var searchDataFn: (arguments: Bundle?) -> EntrySearchData? = { null }
     private var uiLayoutFn: (@Composable (arguments: Bundle?) -> Unit) = { }
 
@@ -170,8 +173,10 @@
             // attributes
             isAllowSearch = isAllowSearch,
             isSearchDataDynamic = isSearchDataDynamic,
+            mutableStatus = mutableStatus,
 
             // functions
+            statusDataImpl = statusDataFn,
             searchDataImpl = searchDataFn,
             uiLayoutImpl = uiLayoutFn,
         )
@@ -201,7 +206,13 @@
         return this
     }
 
+    fun setHasMutableStatus(hasMutableStatus: Boolean): SettingsEntryBuilder {
+        this.mutableStatus = hasMutableStatus
+        return this
+    }
+
     fun setMacro(fn: (arguments: Bundle?) -> EntryMacro): SettingsEntryBuilder {
+        setStatusDataFn { fn(it).getStatusData() }
         setSearchDataFn { fn(it).getSearchData() }
         setUiLayoutFn {
             val macro = remember { fn(it) }
@@ -210,6 +221,11 @@
         return this
     }
 
+    fun setStatusDataFn(fn: (arguments: Bundle?) -> EntryStatusData?): SettingsEntryBuilder {
+        this.statusDataFn = fn
+        return this
+    }
+
     fun setSearchDataFn(fn: (arguments: Bundle?) -> EntrySearchData?): SettingsEntryBuilder {
         this.searchDataFn = fn
         return this
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
index ea20233..e63e4c9 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
@@ -21,10 +21,13 @@
 
 private const val TAG = "EntryRepository"
 private const val MAX_ENTRY_SIZE = 5000
+private const val MAX_ENTRY_DEPTH = 10
 
 data class SettingsPageWithEntry(
     val page: SettingsPage,
     val entries: List<SettingsEntry>,
+    // The inject entry, which to-page is current page.
+    val injectEntry: SettingsEntry,
 )
 
 /**
@@ -42,9 +45,11 @@
         entryMap = mutableMapOf()
         pageWithEntryMap = mutableMapOf()
 
+        val nullPage = SettingsPage.createNull()
         val entryQueue = LinkedList<SettingsEntry>()
         for (page in sppRepository.getAllRootPages()) {
-            val rootEntry = SettingsEntryBuilder.createRoot(owner = page).build()
+            val rootEntry =
+                SettingsEntryBuilder.createRoot(owner = page).setLink(fromPage = nullPage).build()
             if (!entryMap.containsKey(rootEntry.id)) {
                 entryQueue.push(rootEntry)
                 entryMap.put(rootEntry.id, rootEntry)
@@ -57,7 +62,11 @@
             if (page == null || pageWithEntryMap.containsKey(page.id)) continue
             val spp = sppRepository.getProviderOrNull(page.sppName) ?: continue
             val newEntries = spp.buildEntry(page.arguments)
-            pageWithEntryMap[page.id] = SettingsPageWithEntry(page, newEntries)
+            pageWithEntryMap[page.id] = SettingsPageWithEntry(
+                page = page,
+                entries = newEntries,
+                injectEntry = entry
+            )
             for (newEntry in newEntries) {
                 if (!entryMap.containsKey(newEntry.id)) {
                     entryQueue.push(newEntry)
@@ -88,7 +97,29 @@
         return entryMap[entryId]
     }
 
-    fun getEntryPath(entryId: String): String {
-        return "TODO(path_of_$entryId)"
+    private fun getEntryPath(entryId: String): List<SettingsEntry> {
+        val entryPath = ArrayList<SettingsEntry>()
+        var currentEntry = entryMap[entryId]
+        while (currentEntry != null && entryPath.size < MAX_ENTRY_DEPTH) {
+            entryPath.add(currentEntry)
+            val currentPage = currentEntry.containerPage()
+            currentEntry = pageWithEntryMap[currentPage.id]?.injectEntry
+        }
+        return entryPath
+    }
+
+    fun getEntryPathWithDisplayName(entryId: String): List<String> {
+        val entryPath = getEntryPath(entryId)
+        return entryPath.map { it.displayName }
+    }
+
+    fun getEntryPathWithTitle(entryId: String, defaultTitle: String): List<String> {
+        val entryPath = getEntryPath(entryId)
+        return entryPath.map {
+            if (it.toPage == null)
+                defaultTitle
+            else
+                it.toPage.getTitle()!!
+        }
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 07df96e..2fa9229 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -27,6 +27,8 @@
 import com.android.settingslib.spa.framework.util.navLink
 import com.android.settingslib.spa.framework.util.normalize
 
+private const val NULL_PAGE_NAME = "NULL"
+
 /**
  * Defines data to identify a Settings page.
  */
@@ -47,6 +49,10 @@
     val arguments: Bundle? = null,
 ) {
     companion object {
+        fun createNull(): SettingsPage {
+            return create(NULL_PAGE_NAME)
+        }
+
         fun create(
             name: String,
             displayName: String? = null,
@@ -78,16 +84,6 @@
         return sppName == SppName
     }
 
-    fun formatArguments(): String {
-        val normArguments = parameter.normalize(arguments)
-        if (normArguments == null || normArguments.isEmpty) return "[No arguments]"
-        return normArguments.toString().removeRange(0, 6)
-    }
-
-    fun formatDisplayTitle(): String {
-        return "$displayName ${formatArguments()}"
-    }
-
     fun buildRoute(): String {
         return sppName + parameter.navLink(arguments)
     }
@@ -99,12 +95,17 @@
         return false
     }
 
+    fun getTitle(): String? {
+        val sppRepository by SpaEnvironmentFactory.instance.pageProviderRepository
+        return sppRepository.getProviderOrNull(sppName)?.getTitle(arguments)
+    }
+
     fun enterPage() {
         SpaEnvironmentFactory.instance.logger.event(
             id,
             LogEvent.PAGE_ENTER,
             category = LogCategory.FRAMEWORK,
-            details = formatDisplayTitle()
+            details = displayName,
         )
     }
 
@@ -113,7 +114,7 @@
             id,
             LogEvent.PAGE_LEAVE,
             category = LogCategory.FRAMEWORK,
-            details = formatDisplayTitle()
+            details = displayName,
         )
     }
 
@@ -149,6 +150,7 @@
     fun isBrowsable(context: Context?, browseActivityClass: Class<out Activity>?): Boolean {
         return context != null &&
             browseActivityClass != null &&
+            !isCreateBy(NULL_PAGE_NAME) &&
             !hasRuntimeParam()
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index e8a4411..f8963b2 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -41,6 +41,8 @@
     fun Page(arguments: Bundle?)
 
     fun buildEntry(arguments: Bundle?): List<SettingsEntry> = emptyList()
+
+    fun getTitle(arguments: Bundle?): String = displayName ?: name
 }
 
 fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt
new file mode 100644
index 0000000..dbf8836
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/FlowExt.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import android.annotation.SuppressLint
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * *************************************************************************************************
+ * This file was forked from AndroidX:
+ * lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/FlowExt.kt
+ * TODO: Replace with AndroidX when it's usable.
+ */
+
+/**
+ * Collects values from this [StateFlow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * The [StateFlow.value] is used as an initial value. Every time there would be new value posted
+ * into the [StateFlow] the returned [State] will be updated causing recomposition of every
+ * [State.value] usage whenever the [lifecycleOwner]'s lifecycle is at least [minActiveState].
+ *
+ * This [StateFlow] is collected every time the [lifecycleOwner]'s lifecycle reaches the
+ * [minActiveState] Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle
+ * falls below [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.StateFlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this`
+ * flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@SuppressLint("StateFlowValueCalledInComposition")
+@Composable
+fun <T> StateFlow<T>.collectAsStateWithLifecycle(
+    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> = collectAsStateWithLifecycle(
+    initialValue = this.value,
+    lifecycle = lifecycleOwner.lifecycle,
+    minActiveState = minActiveState,
+    context = context
+)
+
+/**
+ * Collects values from this [StateFlow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * The [StateFlow.value] is used as an initial value. Every time there would be new value posted
+ * into the [StateFlow] the returned [State] will be updated causing recomposition of every
+ * [State.value] usage whenever the [lifecycle] is at least [minActiveState].
+ *
+ * This [StateFlow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle
+ * state. The collection stops when [lifecycle] falls below [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.StateFlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param lifecycle [Lifecycle] used to restart collecting `this` flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@SuppressLint("StateFlowValueCalledInComposition")
+@Composable
+fun <T> StateFlow<T>.collectAsStateWithLifecycle(
+    lifecycle: Lifecycle,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> = collectAsStateWithLifecycle(
+    initialValue = this.value,
+    lifecycle = lifecycle,
+    minActiveState = minActiveState,
+    context = context
+)
+
+/**
+ * Collects values from this [Flow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * Every time there would be new value posted into the [Flow] the returned [State] will be updated
+ * causing recomposition of every [State.value] usage whenever the [lifecycleOwner]'s lifecycle is
+ * at least [minActiveState].
+ *
+ * This [Flow] is collected every time the [lifecycleOwner]'s lifecycle reaches the [minActiveState]
+ * Lifecycle state. The collection stops when the [lifecycleOwner]'s lifecycle falls below
+ * [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.FlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param initialValue The initial value given to the returned [State.value].
+ * @param lifecycleOwner [LifecycleOwner] whose `lifecycle` is used to restart collecting `this`
+ * flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@Composable
+fun <T> Flow<T>.collectAsStateWithLifecycle(
+    initialValue: T,
+    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> = collectAsStateWithLifecycle(
+    initialValue = initialValue,
+    lifecycle = lifecycleOwner.lifecycle,
+    minActiveState = minActiveState,
+    context = context
+)
+
+/**
+ * Collects values from this [Flow] and represents its latest value via [State] in a
+ * lifecycle-aware manner.
+ *
+ * Every time there would be new value posted into the [Flow] the returned [State] will be updated
+ * causing recomposition of every [State.value] usage whenever the [lifecycle] is at
+ * least [minActiveState].
+ *
+ * This [Flow] is collected every time [lifecycle] reaches the [minActiveState] Lifecycle
+ * state. The collection stops when [lifecycle] falls below [minActiveState].
+ *
+ * @sample androidx.lifecycle.compose.samples.FlowCollectAsStateWithLifecycle
+ *
+ * Warning: [Lifecycle.State.INITIALIZED] is not allowed in this API. Passing it as a
+ * parameter will throw an [IllegalArgumentException].
+ *
+ * @param initialValue The initial value given to the returned [State.value].
+ * @param lifecycle [Lifecycle] used to restart collecting `this` flow.
+ * @param minActiveState [Lifecycle.State] in which the upstream flow gets collected. The
+ * collection will stop if the lifecycle falls below that state, and will restart if it's in that
+ * state again.
+ * @param context [CoroutineContext] to use for collecting.
+ */
+@Composable
+fun <T> Flow<T>.collectAsStateWithLifecycle(
+    initialValue: T,
+    lifecycle: Lifecycle,
+    minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+    context: CoroutineContext = EmptyCoroutineContext
+): State<T> {
+    return produceState(initialValue, this, lifecycle, minActiveState, context) {
+        lifecycle.repeatOnLifecycle(minActiveState) {
+            if (context == EmptyCoroutineContext) {
+                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
+            } else withContext(context) {
+                this@collectAsStateWithLifecycle.collect { this@produceState.value = it }
+            }
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt
new file mode 100644
index 0000000..8d0313f
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Keyboards.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.KeyboardActionScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+
+/**
+ * An action when run, hides the keyboard if it's open.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun hideKeyboardAction(): KeyboardActionScope.() -> Unit {
+    val keyboardController = LocalSoftwareKeyboardController.current
+    return { keyboardController?.hide() }
+}
+
+/**
+ * Creates a [LazyListState] that is remembered across compositions.
+ *
+ * And when user scrolling the lazy list, hides the keyboard if it's open.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun rememberLazyListStateAndHideKeyboardWhenStartScroll(): LazyListState {
+    val listState = rememberLazyListState()
+    val keyboardController = LocalSoftwareKeyboardController.current
+    LaunchedEffect(listState) {
+        snapshotFlow { listState.isScrollInProgress }
+            .distinctUntilChanged()
+            .filter { it }
+            .collect { keyboardController?.hide() }
+    }
+    return listState
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt
new file mode 100644
index 0000000..1b33dd6
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/OverridableFlow.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.receiveAsFlow
+
+/**
+ * A flow which result is overridable.
+ */
+class OverridableFlow<T>(flow: Flow<T>) {
+    private val overrideChannel = Channel<T>()
+
+    val flow = merge(overrideChannel.receiveAsFlow(), flow)
+
+    fun override(value: T) {
+        overrideChannel.trySend(value)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
index bf33857..4df7794 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/Pager.kt
@@ -40,7 +40,6 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.filter
@@ -214,6 +213,7 @@
             horizontalAlignment = horizontalAlignment,
             reverseLayout = reverseLayout,
             contentPadding = contentPadding,
+            userScrollEnabled = false,
             modifier = modifier,
         ) {
             items(
@@ -241,6 +241,7 @@
             horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
             reverseLayout = reverseLayout,
             contentPadding = contentPadding,
+            userScrollEnabled = false,
             modifier = modifier,
         ) {
             items(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
index 3015080..9eaa88a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugActivity.kt
@@ -120,12 +120,11 @@
         val allPageWithEntry = remember { entryRepository.getAllPageWithEntry() }
         RegularScaffold(title = "All Pages (${allPageWithEntry.size})") {
             for (pageWithEntry in allPageWithEntry) {
+                val page = pageWithEntry.page
                 Preference(object : PreferenceModel {
-                    override val title =
-                        "${pageWithEntry.page.displayName} (${pageWithEntry.entries.size})"
-                    override val summary = pageWithEntry.page.formatArguments().toState()
-                    override val onClick =
-                        navigator(route = ROUTE_PAGE + "/${pageWithEntry.page.id}")
+                    override val title = "${page.debugBrief()} (${pageWithEntry.entries.size})"
+                    override val summary = page.debugArguments().toState()
+                    override val onClick = navigator(route = ROUTE_PAGE + "/${page.id}")
                 })
             }
         }
@@ -146,16 +145,16 @@
         val entryRepository by spaEnvironment.entryRepository
         val id = arguments!!.getString(PARAM_NAME_PAGE_ID, "")
         val pageWithEntry = entryRepository.getPageWithEntry(id)!!
-        RegularScaffold(title = "Page - ${pageWithEntry.page.displayName}") {
-            Text(text = "id = ${pageWithEntry.page.id}")
-            Text(text = pageWithEntry.page.formatArguments())
+        val page = pageWithEntry.page
+        RegularScaffold(title = "Page - ${page.debugBrief()}") {
+            Text(text = "id = ${page.id}")
+            Text(text = page.debugArguments())
             Text(text = "Entry size: ${pageWithEntry.entries.size}")
             Preference(model = object : PreferenceModel {
                 override val title = "open page"
                 override val enabled =
-                    pageWithEntry.page.isBrowsable(context, spaEnvironment.browseActivityClass)
-                        .toState()
-                override val onClick = openPage(pageWithEntry.page)
+                    page.isBrowsable(context, spaEnvironment.browseActivityClass).toState()
+                override val onClick = openPage(page)
             })
             EntryList(pageWithEntry.entries)
         }
@@ -167,8 +166,8 @@
         val entryRepository by spaEnvironment.entryRepository
         val id = arguments!!.getString(PARAM_NAME_ENTRY_ID, "")
         val entry = entryRepository.getEntry(id)!!
-        val entryContent = remember { entry.formatContent() }
-        RegularScaffold(title = "Entry - ${entry.displayTitle()}") {
+        val entryContent = remember { entry.debugContent(entryRepository) }
+        RegularScaffold(title = "Entry - ${entry.debugBrief()}") {
             Preference(model = object : PreferenceModel {
                 override val title = "open entry"
                 override val enabled =
@@ -184,7 +183,7 @@
     private fun EntryList(entries: Collection<SettingsEntry>) {
         for (entry in entries) {
             Preference(object : PreferenceModel {
-                override val title = entry.displayTitle()
+                override val title = entry.debugBrief()
                 override val summary =
                     "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState()
                 override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}")
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugFormat.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugFormat.kt
new file mode 100644
index 0000000..538d2b5
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugFormat.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.debug
+
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.EntryStatusData
+import com.android.settingslib.spa.framework.common.SettingsEntry
+import com.android.settingslib.spa.framework.common.SettingsEntryRepository
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.util.normalize
+
+private fun EntrySearchData.debugContent(): String {
+    val content = listOf(
+        "search_title = $title",
+        "search_keyword = $keyword",
+    )
+    return content.joinToString("\n")
+}
+
+private fun EntryStatusData.debugContent(): String {
+    val content = listOf(
+        "is_disabled = $isDisabled",
+        "is_switch_off = $isSwitchOff",
+    )
+    return content.joinToString("\n")
+}
+
+fun SettingsPage.debugArguments(): String {
+    val normArguments = parameter.normalize(arguments)
+    if (normArguments == null || normArguments.isEmpty) return "[No arguments]"
+    return normArguments.toString().removeRange(0, 6)
+}
+
+fun SettingsPage.debugBrief(): String {
+    return displayName
+}
+
+fun SettingsEntry.debugBrief(): String {
+    return "${owner.displayName}:$displayName"
+}
+
+fun SettingsEntry.debugContent(entryRepository: SettingsEntryRepository): String {
+    val searchData = getSearchData()
+    val statusData = getStatusData()
+    val entryPathWithName = entryRepository.getEntryPathWithDisplayName(id)
+    val entryPathWithTitle = entryRepository.getEntryPathWithTitle(id,
+        searchData?.title ?: displayName)
+    val content = listOf(
+        "------ STATIC ------",
+        "id = $id",
+        "owner = ${owner.debugBrief()} ${owner.debugArguments()}",
+        "linkFrom = ${fromPage?.debugBrief()} ${fromPage?.debugArguments()}",
+        "linkTo = ${toPage?.debugBrief()} ${toPage?.debugArguments()}",
+        "hierarchy_path = $entryPathWithName",
+        "------ SEARCH ------",
+        "search_path = $entryPathWithTitle",
+        searchData?.debugContent() ?: "no search data",
+        statusData?.debugContent() ?: "no status data",
+    )
+    return content.joinToString("\n")
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
index 6c27109..399278d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/debug/DebugProvider.kt
@@ -151,9 +151,9 @@
                 .add(ColumnEnum.PAGE_ID.id, page.id)
                 .add(ColumnEnum.PAGE_NAME.id, page.displayName)
                 .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
+                .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
                 .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
                 .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
-                .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
         }
         return cursor
     }
@@ -170,6 +170,8 @@
                 .add(ColumnEnum.ENTRY_NAME.id, entry.displayName)
                 .add(ColumnEnum.ENTRY_ROUTE.id, entry.containerPage().buildRoute())
                 .add(ColumnEnum.ENTRY_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
+                .add(ColumnEnum.ENTRY_HIERARCHY_PATH.id,
+                    entryRepository.getEntryPathWithDisplayName(entry.id))
         }
         return cursor
     }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
index 3fa8c65..52c4893 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt
@@ -44,3 +44,6 @@
 
 val ColorScheme.divider: Color
     get() = onSurface.copy(SettingsOpacity.Divider)
+
+val ColorScheme.surfaceTone: Color
+    get() = primary.copy(SettingsOpacity.SurfaceTone)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt
new file mode 100644
index 0000000..8e8805a
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsFontFamily.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalTextApi::class)
+
+package com.android.settingslib.spa.framework.theme
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.font.DeviceFontFamilyName
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+
+internal data class SettingsFontFamily(
+    val brand: FontFamily = FontFamily.Default,
+    val plain: FontFamily = FontFamily.Default,
+)
+
+private fun Context.getSettingsFontFamily(inInspection: Boolean): SettingsFontFamily {
+    if (inInspection) {
+        return SettingsFontFamily()
+    }
+    return SettingsFontFamily(
+        brand = FontFamily(
+            Font(getFontFamilyName("config_headlineFontFamily"), FontWeight.Normal),
+            Font(getFontFamilyName("config_headlineFontFamilyMedium"), FontWeight.Medium),
+        ),
+        plain = FontFamily(
+            Font(getFontFamilyName("config_bodyFontFamily"), FontWeight.Normal),
+            Font(getFontFamilyName("config_bodyFontFamilyMedium"), FontWeight.Medium),
+        ),
+    )
+}
+
+private fun Context.getFontFamilyName(configName: String): DeviceFontFamilyName {
+    @SuppressLint("DiscouragedApi")
+    val configId = resources.getIdentifier(configName, "string", "android")
+    return DeviceFontFamilyName(resources.getString(configId))
+}
+
+@Composable
+internal fun rememberSettingsFontFamily(): SettingsFontFamily {
+    val context = LocalContext.current
+    val inInspection = LocalInspectionMode.current
+    return remember { context.getSettingsFontFamily(inInspection) }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
index 11af6ce..c8faef6 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt
@@ -20,4 +20,6 @@
     const val Full = 1f
     const val Disabled = 0.38f
     const val Divider = 0.2f
+    const val SurfaceTone = 0.14f
+    const val Hint = 0.9f
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 07f09ba..03699bf 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -20,14 +20,13 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.em
 import androidx.compose.ui.unit.sp
 
-private class SettingsTypography {
-    private val brand = FontFamily.Default
-    private val plain = FontFamily.Default
+private class SettingsTypography(settingsFontFamily: SettingsFontFamily) {
+    private val brand = settingsFontFamily.brand
+    private val plain = settingsFontFamily.plain
 
     val typography = Typography(
         displayLarge = TextStyle(
@@ -140,5 +139,6 @@
 
 @Composable
 internal fun rememberSettingsTypography(): Typography {
-    return remember { SettingsTypography().typography }
+    val settingsFontFamily = rememberSettingsFontFamily()
+    return remember { SettingsTypography(settingsFontFamily).typography }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
index 6c7432e..8d0a35c 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/WidgetLogger.kt
@@ -23,7 +23,7 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 
 @Composable
-fun LogEntryEvent(): (event: LogEvent) -> Unit {
+fun logEntryEvent(): (event: LogEvent) -> Unit {
     val entryId = LocalEntryDataProvider.current.entryId ?: return {}
     return {
         SpaEnvironmentFactory.instance.logger.event(entryId, it, category = LogCategory.VIEW)
@@ -31,9 +31,9 @@
 }
 
 @Composable
-fun WrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? {
+fun wrapOnClickWithLog(onClick: (() -> Unit)?): (() -> Unit)? {
     if (onClick == null) return null
-    val logEvent = LogEntryEvent()
+    val logEvent = logEntryEvent()
     return {
         logEvent(LogEvent.ENTRY_CLICK)
         onClick()
@@ -41,9 +41,9 @@
 }
 
 @Composable
-fun WrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? {
+fun wrapOnSwitchWithLog(onSwitch: ((checked: Boolean) -> Unit)?): ((checked: Boolean) -> Unit)? {
     if (onSwitch == null) return null
-    val logEvent = LogEntryEvent()
+    val logEvent = logEntryEvent()
     return {
         val event = if (it) LogEvent.ENTRY_SWITCH_ON else LogEvent.ENTRY_SWITCH_OFF
         logEvent(event)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
index 9a34dbf..6135203 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
@@ -72,7 +72,7 @@
 }
 
 @Composable
-private fun BaseIcon(
+internal fun BaseIcon(
     icon: @Composable (() -> Unit)?,
     modifier: Modifier,
     paddingStart: Dp,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
index 3e04b16..db95e23 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
@@ -28,7 +28,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsShape
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 @Composable
 fun MainSwitchPreference(model: SwitchPreferenceModel) {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
index 7c0116a..895edf7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
@@ -24,11 +24,12 @@
 import androidx.compose.ui.graphics.vector.ImageVector
 import com.android.settingslib.spa.framework.common.EntryMacro
 import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.EntryStatusData
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.compose.stateOf
-import com.android.settingslib.spa.framework.util.WrapOnClickWithLog
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.framework.util.wrapOnClickWithLog
 import com.android.settingslib.spa.widget.ui.createSettingsIcon
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 data class SimplePreferenceMacro(
     val title: String,
@@ -55,6 +56,10 @@
             keyword = searchKeywords
         )
     }
+
+    override fun getStatusData(): EntryStatusData {
+        return EntryStatusData(isDisabled = false)
+    }
 }
 
 /**
@@ -107,7 +112,7 @@
     model: PreferenceModel,
     singleLineSummary: Boolean = false,
 ) {
-    val onClickWithLog = WrapOnClickWithLog(model.onClick)
+    val onClickWithLog = wrapOnClickWithLog(model.onClick)
     val modifier = remember(model.enabled.value) {
         if (onClickWithLog != null) {
             Modifier.clickable(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt
new file mode 100644
index 0000000..b8c59ad
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ProgressBarPreference.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.preference
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.LinearProgressBar
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+/**
+ * The widget model for [ProgressBarPreference] widget.
+ */
+interface ProgressBarPreferenceModel {
+    /**
+     * The title of this [ProgressBarPreference].
+     */
+    val title: String
+
+    /**
+     * The progress fraction of the ProgressBar. Should be float in range [0f, 1f]
+     */
+    val progress: Float
+
+    /**
+     * The icon image for [ProgressBarPreference]. If not specified, hides the icon by default.
+     */
+    val icon: ImageVector?
+        get() = null
+
+    /**
+     * The height of the ProgressBar.
+     */
+    val height: Float
+        get() = 4f
+
+    /**
+     * Indicates whether to use rounded corner for the progress bars.
+     */
+    val roundedCorner: Boolean
+        get() = true
+}
+
+/**
+ * Progress bar preference widget.
+ *
+ * Data is provided through [ProgressBarPreferenceModel].
+ */
+@Composable
+fun ProgressBarPreference(model: ProgressBarPreferenceModel) {
+    ProgressBarPreference(
+        title = model.title,
+        progress = model.progress,
+        icon = model.icon,
+        height = model.height,
+        roundedCorner = model.roundedCorner,
+    )
+}
+
+/**
+ * Progress bar with data preference widget.
+ */
+@Composable
+fun ProgressBarWithDataPreference(model: ProgressBarPreferenceModel, data: String) {
+    val icon = model.icon
+    ProgressBarWithDataPreference(
+        title = model.title,
+        data = data,
+        progress = model.progress,
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+        height = model.height,
+        roundedCorner = model.roundedCorner,
+    )
+}
+
+@Composable
+internal fun ProgressBarPreference(
+    title: String,
+    progress: Float,
+    icon: ImageVector? = null,
+    height: Float = 4f,
+    roundedCorner: Boolean = true,
+) {
+    BaseLayout(
+        title = title,
+        subTitle = {
+            LinearProgressBar(progress, height, roundedCorner)
+        },
+        icon = if (icon != null) ({
+            Icon(imageVector = icon, contentDescription = null)
+        }) else null,
+    )
+}
+
+
+@Composable
+internal fun ProgressBarWithDataPreference(
+    title: String,
+    data: String,
+    progress: Float,
+    icon: (@Composable () -> Unit)? = null,
+    height: Float = 4f,
+    roundedCorner: Boolean = true,
+) {
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(end = SettingsDimension.itemPaddingEnd),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        BaseIcon(icon, Modifier, SettingsDimension.itemPaddingStart)
+        TitleWithData(
+            title = title,
+            data = data,
+            subTitle = {
+                LinearProgressBar(progress, height, roundedCorner)
+            },
+            modifier = Modifier
+                .weight(1f)
+                .padding(vertical = SettingsDimension.itemPaddingVertical),
+        )
+    }
+}
+
+@Composable
+private fun TitleWithData(
+    title: String,
+    data: String,
+    subTitle: @Composable () -> Unit,
+    modifier: Modifier
+) {
+    Column(modifier) {
+        Row {
+            Box(modifier = Modifier.weight(1f)) {
+                SettingsTitle(title)
+            }
+            Text(
+                text = data,
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                style = MaterialTheme.typography.titleMedium,
+            )
+        }
+        subTitle()
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
index 7bca38f..4ee2af0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SliderPreference.kt
@@ -31,8 +31,8 @@
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSlider
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 /**
  * The widget model for [SliderPreference] widget.
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
index 592a99f..2d60619 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
@@ -31,9 +31,9 @@
 import com.android.settingslib.spa.framework.compose.toState
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
+import com.android.settingslib.spa.widget.util.EntryHighlight
 
 /**
  * The widget model for [SwitchPreference] widget.
@@ -104,7 +104,7 @@
 ) {
     val checkedValue = checked.value
     val indication = LocalIndication.current
-    val onChangeWithLog = WrapOnSwitchWithLog(onCheckedChange)
+    val onChangeWithLog = wrapOnSwitchWithLog(onCheckedChange)
     val modifier = remember(checkedValue, changeable.value) {
         if (checkedValue != null && onChangeWithLog != null) {
             Modifier.toggleable(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
index 63de2c8..fbfcaaa 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt
@@ -17,7 +17,7 @@
 package com.android.settingslib.spa.widget.preference
 
 import androidx.compose.runtime.Composable
-import com.android.settingslib.spa.framework.util.EntryHighlight
+import com.android.settingslib.spa.widget.util.EntryHighlight
 import com.android.settingslib.spa.widget.ui.SettingsSwitch
 
 @Composable
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 6a88f2d..764973f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -16,9 +16,12 @@
 
 package com.android.settingslib.spa.widget.scaffold
 
+import androidx.appcompat.R
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material.icons.outlined.FindInPage
 import androidx.compose.material.icons.outlined.MoreVert
 import androidx.compose.material3.DropdownMenu
 import androidx.compose.material3.Icon
@@ -31,17 +34,23 @@
 import androidx.compose.ui.res.stringResource
 import com.android.settingslib.spa.framework.compose.LocalNavController
 
+/** Action that navigates back to last page. */
 @Composable
 internal fun NavigateBack() {
     val navController = LocalNavController.current
-    val contentDescription = stringResource(
-        id = androidx.appcompat.R.string.abc_action_bar_up_description,
-    )
+    val contentDescription = stringResource(R.string.abc_action_bar_up_description)
     BackAction(contentDescription) {
         navController.navigateBack()
     }
 }
 
+/** Action that collapses the search bar. */
+@Composable
+internal fun CollapseAction(onClick: () -> Unit) {
+    val contentDescription = stringResource(R.string.abc_toolbar_collapse_description)
+    BackAction(contentDescription, onClick)
+}
+
 @Composable
 private fun BackAction(contentDescription: String, onClick: () -> Unit) {
     IconButton(onClick) {
@@ -52,6 +61,28 @@
     }
 }
 
+/** Action that expends the search bar. */
+@Composable
+internal fun SearchAction(onClick: () -> Unit) {
+    IconButton(onClick) {
+        Icon(
+            imageVector = Icons.Outlined.FindInPage,
+            contentDescription = stringResource(R.string.search_menu_title),
+        )
+    }
+}
+
+/** Action that clear the search query. */
+@Composable
+internal fun ClearAction(onClick: () -> Unit) {
+    IconButton(onClick) {
+        Icon(
+            imageVector = Icons.Outlined.Clear,
+            contentDescription = stringResource(R.string.abc_searchview_description_clear),
+        )
+    }
+}
+
 @Composable
 fun MoreOptionsAction(
     content: @Composable ColumnScope.(onDismissRequest: () -> Unit) -> Unit,
@@ -71,9 +102,7 @@
     IconButton(onClick) {
         Icon(
             imageVector = Icons.Outlined.MoreVert,
-            contentDescription = stringResource(
-                id = androidx.appcompat.R.string.abc_action_menu_overflow_description,
-            )
+            contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
         )
     }
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
new file mode 100644
index 0000000..4f83ad6
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SearchScaffold.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.activity.compose.BackHandler
+import androidx.appcompat.R
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.settingslib.spa.framework.compose.hideKeyboardAction
+import com.android.settingslib.spa.framework.theme.SettingsOpacity
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+/**
+ * A [Scaffold] which content is can be full screen, and with a search feature built-in.
+ */
+@Composable
+fun SearchScaffold(
+    title: String,
+    actions: @Composable RowScope.() -> Unit = {},
+    content: @Composable (searchQuery: State<String>) -> Unit,
+) {
+    val viewModel: SearchScaffoldViewModel = viewModel()
+
+    Scaffold(
+        topBar = {
+            SearchableTopAppBar(
+                title = title,
+                actions = actions,
+                searchQuery = viewModel.searchQuery,
+            ) { viewModel.searchQuery = it }
+        },
+    ) { paddingValues ->
+        Box(
+            Modifier
+                .padding(paddingValues)
+                .fillMaxSize()
+        ) {
+            val searchQuery = remember {
+                derivedStateOf { viewModel.searchQuery?.text ?: "" }
+            }
+            content(searchQuery)
+        }
+    }
+}
+
+internal class SearchScaffoldViewModel : ViewModel() {
+    var searchQuery: TextFieldValue? by mutableStateOf(null)
+}
+
+@Composable
+private fun SearchableTopAppBar(
+    title: String,
+    actions: @Composable RowScope.() -> Unit,
+    searchQuery: TextFieldValue?,
+    onSearchQueryChange: (TextFieldValue?) -> Unit,
+) {
+    if (searchQuery != null) {
+        SearchTopAppBar(
+            query = searchQuery,
+            onQueryChange = onSearchQueryChange,
+            onClose = { onSearchQueryChange(null) },
+            actions = actions,
+        )
+    } else {
+        SettingsTopAppBar(title) {
+            SearchAction { onSearchQueryChange(TextFieldValue()) }
+            actions()
+        }
+    }
+}
+
+@Composable
+private fun SearchTopAppBar(
+    query: TextFieldValue,
+    onQueryChange: (TextFieldValue) -> Unit,
+    onClose: () -> Unit,
+    actions: @Composable RowScope.() -> Unit = {},
+) {
+    TopAppBar(
+        title = { SearchBox(query, onQueryChange) },
+        modifier = Modifier.statusBarsPadding(),
+        navigationIcon = { CollapseAction(onClose) },
+        actions = {
+            if (query.text.isNotEmpty()) {
+                ClearAction { onQueryChange(TextFieldValue()) }
+            }
+            actions()
+        },
+        colors = settingsTopAppBarColors(),
+    )
+    BackHandler { onClose() }
+}
+
+@Composable
+private fun SearchBox(query: TextFieldValue, onQueryChange: (TextFieldValue) -> Unit) {
+    val focusRequester = remember { FocusRequester() }
+    val textStyle = MaterialTheme.typography.bodyLarge
+    TextField(
+        value = query,
+        onValueChange = onQueryChange,
+        modifier = Modifier
+            .fillMaxWidth()
+            .focusRequester(focusRequester),
+        textStyle = textStyle,
+        placeholder = {
+            Text(
+                text = stringResource(R.string.abc_search_hint),
+                modifier = Modifier.alpha(SettingsOpacity.Hint),
+                style = textStyle,
+            )
+        },
+        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+        keyboardActions = KeyboardActions(onSearch = hideKeyboardAction()),
+        singleLine = true,
+        colors = TextFieldDefaults.textFieldColors(
+            containerColor = Color.Transparent,
+            focusedIndicatorColor = Color.Transparent,
+            unfocusedIndicatorColor = Color.Transparent,
+        ),
+    )
+
+    LaunchedEffect(focusRequester) {
+        focusRequester.requestFocus()
+    }
+}
+
+@Preview
+@Composable
+private fun SearchTopAppBarPreview() {
+    SettingsTheme {
+        SearchTopAppBar(query = TextFieldValue(), onQueryChange = {}, onClose = {}) {}
+    }
+}
+
+@Preview
+@Composable
+private fun SearchScaffoldPreview() {
+    SettingsTheme {
+        SearchScaffold(title = "App notifications") {}
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
index d17e464..3bc3dd7 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt
@@ -18,17 +18,10 @@
 
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SmallTopAppBar
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 
 /**
@@ -42,32 +35,11 @@
     content: @Composable (PaddingValues) -> Unit,
 ) {
     Scaffold(
-        topBar = {
-            SmallTopAppBar(
-                title = {
-                    Text(
-                        text = title,
-                        modifier = Modifier.padding(SettingsDimension.itemPaddingAround),
-                        overflow = TextOverflow.Ellipsis,
-                        maxLines = 1,
-                    )
-                },
-                navigationIcon = { NavigateBack() },
-                actions = actions,
-                colors = settingsTopAppBarColors(),
-            )
-        },
+        topBar = { SettingsTopAppBar(title, actions) },
         content = content,
     )
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-internal fun settingsTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
-    containerColor = SettingsTheme.colorScheme.surfaceHeader,
-    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
-)
-
 @Preview
 @Composable
 private fun SettingsScaffoldPreview() {
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
new file mode 100644
index 0000000..9353520
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTopAppBar.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun SettingsTopAppBar(
+    title: String,
+    actions: @Composable RowScope.() -> Unit,
+) {
+    TopAppBar(
+        title = {
+            Text(
+                text = title,
+                modifier = Modifier.padding(SettingsDimension.itemPaddingAround),
+                overflow = TextOverflow.Ellipsis,
+                maxLines = 1,
+            )
+        },
+        navigationIcon = { NavigateBack() },
+        actions = actions,
+        colors = settingsTopAppBarColors(),
+    )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun settingsTopAppBarColors() = TopAppBarDefaults.smallTopAppBarColors(
+    containerColor = SettingsTheme.colorScheme.surfaceHeader,
+    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
+)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt
new file mode 100644
index 0000000..1741f13
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.absoluteOffset
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Indeterminate linear progress bar. Expresses an unspecified wait time.
+ */
+@Composable
+fun LinearLoadingBar(
+    isLoading: Boolean,
+    xOffset: Dp = 0.dp,
+    yOffset: Dp = 0.dp
+) {
+    if (isLoading) {
+        LinearProgressIndicator(
+            modifier = Modifier
+                .fillMaxWidth()
+                .absoluteOffset(xOffset, yOffset)
+        )
+    }
+}
+
+/**
+ * Indeterminate circular progress bar. Expresses an unspecified wait time.
+ */
+@Composable
+fun CircularLoadingBar(isLoading: Boolean) {
+    if (isLoading) {
+        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            CircularProgressIndicator()
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt
new file mode 100644
index 0000000..5d8502d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/ProgressBar.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.progressSemantics
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.dp
+
+/**
+ * Determinate linear progress bar. Displays the current progress of the whole process.
+ *
+ * Rounded corner is supported and enabled by default.
+ */
+@Composable
+fun LinearProgressBar(
+    progress: Float,
+    height: Float = 4f,
+    roundedCorner: Boolean = true
+) {
+    Box(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) {
+        val color = MaterialTheme.colorScheme.onSurface
+        val trackColor = MaterialTheme.colorScheme.surfaceVariant
+        Canvas(
+            Modifier
+                .progressSemantics(progress)
+                .fillMaxWidth()
+                .height(height.dp)
+        ) {
+            drawLinearBarTrack(trackColor, roundedCorner)
+            drawLinearBar(progress, color, roundedCorner)
+        }
+    }
+}
+
+private fun DrawScope.drawLinearBar(
+    endFraction: Float,
+    color: Color,
+    roundedCorner: Boolean
+) {
+    val width = endFraction * size.width
+    drawRoundRect(
+        color = color,
+        size = Size(width, size.height),
+        cornerRadius = if (roundedCorner) CornerRadius(
+            size.height / 2,
+            size.height / 2
+        ) else CornerRadius.Zero,
+    )
+}
+
+private fun DrawScope.drawLinearBarTrack(
+    color: Color,
+    roundedCorner: Boolean
+) = drawLinearBar(1f, color, roundedCorner)
+
+/**
+ * Determinate circular progress bar. Displays the current progress of the whole process.
+ *
+ * Displayed in default material3 style, and rounded corner is not supported.
+ */
+@Composable
+fun CircularProgressBar(progress: Float, radius: Float = 40f) {
+    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+        CircularProgressIndicator(
+            progress = progress,
+            modifier = Modifier.size(radius.dp, radius.dp)
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
index d8455e4..48fec3b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/SettingsSlider.kt
@@ -16,13 +16,16 @@
 
 package com.android.settingslib.spa.widget.ui
 
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.theme.surfaceTone
 import kotlin.math.roundToInt
 
 @Composable
@@ -45,5 +48,8 @@
         valueRange = valueRange.first.toFloat()..valueRange.last.toFloat(),
         steps = if (showSteps) (valueRange.count() - 2) else 0,
         onValueChangeFinished = onValueChangeFinished,
+        colors = SliderDefaults.colors(
+            inactiveTrackColor = MaterialTheme.colorScheme.surfaceTone
+        )
     )
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
index 82ab0be..9831b91 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Switch.kt
@@ -16,30 +16,26 @@
 
 package com.android.settingslib.spa.widget.ui
 
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Switch
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
-import com.android.settingslib.spa.framework.util.WrapOnSwitchWithLog
+import com.android.settingslib.spa.framework.util.wrapOnSwitchWithLog
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SettingsSwitch(
     checked: State<Boolean?>,
     changeable: State<Boolean>,
     onCheckedChange: ((newChecked: Boolean) -> Unit)? = null,
 ) {
-    // TODO: Replace Checkbox with Switch when the androidx.compose.material3_material3 library is
-    //       updated to date.
     val checkedValue = checked.value
     if (checkedValue != null) {
-        Checkbox(
+        Switch(
             checked = checkedValue,
-            onCheckedChange = WrapOnSwitchWithLog(onCheckedChange),
+            onCheckedChange = wrapOnSwitchWithLog(onCheckedChange),
             enabled = changeable.value,
         )
     } else {
-        Checkbox(
+        Switch(
             checked = false,
             onCheckedChange = null,
             enabled = false,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
similarity index 96%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
index 8e24ce0..652e54d 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/EntryHighlight.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/util/EntryHighlight.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework.util
+package com.android.settingslib.spa.widget.util
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index b43bf18..4b4c6a3 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -55,11 +55,6 @@
     composeOptions {
         kotlinCompilerExtensionVersion jetpack_compose_compiler_version
     }
-    packagingOptions {
-        resources {
-            excludes += '/META-INF/{AL2.0,LGPL2.1}'
-        }
-    }
 }
 
 dependencies {
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt
new file mode 100644
index 0000000..c94572b
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/OverridableFlowTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class OverridableFlowTest {
+
+    @Test
+    fun noOverride() = runTest {
+        val overridableFlow = OverridableFlow(flowOf(true))
+
+        launch {
+            val values = collectValues(overridableFlow.flow)
+            assertThat(values).containsExactly(true)
+        }
+    }
+
+    @Test
+    fun whenOverride() = runTest {
+        val overridableFlow = OverridableFlow(flowOf(true))
+
+        overridableFlow.override(false)
+
+        launch {
+            val values = collectValues(overridableFlow.flow)
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+    }
+
+    private suspend fun <T> collectValues(flow: Flow<T>): List<T> = withTimeout(500) {
+        val flowValues = mutableListOf<T>()
+        flow.toList(flowValues)
+        flowValues
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt
new file mode 100644
index 0000000..5611f8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.preference
+
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ProgressBarPreferenceTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun title_displayed() {
+        composeTestRule.setContent {
+            ProgressBarPreference(object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            })
+        }
+        composeTestRule.onNodeWithText("Title").assertIsDisplayed()
+    }
+
+    @Test
+    fun data_displayed() {
+        composeTestRule.setContent {
+            ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            }, data = "Data")
+        }
+        composeTestRule.onNodeWithText("Title").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Data").assertIsDisplayed()
+    }
+
+    @Test
+    fun progressBar_displayed() {
+        composeTestRule.setContent {
+            ProgressBarPreference(object : ProgressBarPreferenceModel {
+                override val title = "Title"
+                override val progress = 0.2f
+            })
+        }
+
+        fun progressEqualsTo(progress: Float): SemanticsMatcher =
+            SemanticsMatcher.expectValue(
+                ProgressBarRangeInfo,
+                ProgressBarRangeInfo(progress, 0f..1f, 0)
+            )
+        composeTestRule.onNode(progressEqualsTo(0.2f)).assertIsDisplayed()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
index 7ae1175..3e5dd52 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/SliderPreferenceTest.kt
@@ -16,6 +16,9 @@
 
 package com.android.settingslib.spa.widget.preference
 
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo
+import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
@@ -41,5 +44,20 @@
         composeTestRule.onNodeWithText("Slider").assertIsDisplayed()
     }
 
-    // TODO: Add more unit tests for SliderPreference widget.
+    @Test
+    fun slider_displayed() {
+        composeTestRule.setContent {
+            SliderPreference(object : SliderPreferenceModel {
+                override val title = "Slider"
+                override val initValue = 40
+            })
+        }
+
+        fun progressEqualsTo(progress: Float): SemanticsMatcher =
+            SemanticsMatcher.expectValue(
+                ProgressBarRangeInfo,
+                ProgressBarRangeInfo(progress, 0f..100f, 0)
+            )
+        composeTestRule.onNode(progressEqualsTo(40f)).assertIsDisplayed()
+    }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
new file mode 100644
index 0000000..ec3379d
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SearchScaffoldTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import android.content.Context
+import androidx.appcompat.R
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.State
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SearchScaffoldTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun initialState_titleIsDisplayed() {
+        composeTestRule.setContent {
+            SearchScaffold(title = TITLE) {}
+        }
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun initialState_clearButtonNotExist() {
+        setContent()
+
+        onClearButton().assertDoesNotExist()
+    }
+
+    @Test
+    fun initialState_searchQueryIsEmpty() {
+        val searchQuery = setContent()
+
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    @Test
+    fun canEnterSearchMode() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+
+        composeTestRule.onNodeWithText(TITLE).assertDoesNotExist()
+        onSearchHint().assertIsDisplayed()
+        onClearButton().assertDoesNotExist()
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    @Test
+    fun canExitSearchMode() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+        composeTestRule.onNodeWithContentDescription(
+            context.getString(R.string.abc_toolbar_collapse_description)
+        ).performClick()
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+        onSearchHint().assertDoesNotExist()
+        onClearButton().assertDoesNotExist()
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    @Test
+    fun canEnterSearchQuery() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+        onSearchHint().performTextInput(QUERY)
+
+        onClearButton().assertIsDisplayed()
+        assertThat(searchQuery.value).isEqualTo(QUERY)
+    }
+
+    @Test
+    fun canClearSearchQuery() {
+        val searchQuery = setContent()
+
+        clickSearchButton()
+        onSearchHint().performTextInput(QUERY)
+        onClearButton().performClick()
+
+        onClearButton().assertDoesNotExist()
+        assertThat(searchQuery.value).isEqualTo("")
+    }
+
+    private fun setContent(): State<String> {
+        lateinit var actualSearchQuery: State<String>
+        composeTestRule.setContent {
+            SearchScaffold(title = TITLE) { searchQuery ->
+                SideEffect {
+                    actualSearchQuery = searchQuery
+                }
+            }
+        }
+        return actualSearchQuery
+    }
+
+    private fun clickSearchButton() {
+        composeTestRule.onNodeWithContentDescription(
+            context.getString(R.string.search_menu_title)
+        ).performClick()
+    }
+
+    private fun onSearchHint() = composeTestRule.onNodeWithText(
+        context.getString(R.string.abc_search_hint)
+    )
+
+    private fun onClearButton() = composeTestRule.onNodeWithContentDescription(
+        context.getString(R.string.abc_searchview_description_clear)
+    )
+
+    private companion object {
+        const val TITLE = "title"
+        const val QUERY = "query"
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
index 1dc52cb..fd723dd 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/Contexts.kt
@@ -1,15 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.settingslib.spaprivileged.framework.common
 
+import android.app.AlarmManager
+import android.app.AppOpsManager
 import android.app.admin.DevicePolicyManager
 import android.app.usage.StorageStatsManager
+import android.apphibernation.AppHibernationManager
 import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.os.UserHandle
 import android.os.UserManager
+import android.permission.PermissionControllerManager
 
-/** The [UserManager] instance. */
-val Context.userManager get() = getSystemService(UserManager::class.java)!!
+/** The [AlarmManager] instance. */
+val Context.alarmManager get() = getSystemService(AlarmManager::class.java)!!
+
+/** The [AppHibernationManager] instance. */
+val Context.appHibernationManager get() = getSystemService(AppHibernationManager::class.java)!!
+
+/** The [AppOpsManager] instance. */
+val Context.appOpsManager get() = getSystemService(AppOpsManager::class.java)!!
 
 /** The [DevicePolicyManager] instance. */
 val Context.devicePolicyManager get() = getSystemService(DevicePolicyManager::class.java)!!
 
+/** The [DomainVerificationManager] instance. */
+val Context.domainVerificationManager
+    get() = getSystemService(DomainVerificationManager::class.java)!!
+
+/** The [PermissionControllerManager] instance. */
+val Context.permissionControllerManager
+    get() = getSystemService(PermissionControllerManager::class.java)!!
+
 /** The [StorageStatsManager] instance. */
 val Context.storageStatsManager get() = getSystemService(StorageStatsManager::class.java)!!
+
+/** The [UserManager] instance. */
+val Context.userManager get() = getSystemService(UserManager::class.java)!!
+
+/** Gets a new [Context] for the given [UserHandle]. */
+fun Context.asUser(userHandle: UserHandle): Context = createContextAsUser(userHandle, 0)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
index c5ad181..408b9df 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
@@ -29,6 +28,7 @@
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.settingslib.spa.framework.compose.LogCompositions
 import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer
+import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll
 import com.android.settingslib.spa.framework.compose.toState
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.widget.ui.PlaceholderTitle
@@ -76,7 +76,7 @@
         }
         LazyColumn(
             modifier = Modifier.fillMaxSize(),
-            state = rememberLazyListState(),
+            state = rememberLazyListStateAndHideKeyboardWhenStartScroll(),
             contentPadding = PaddingValues(bottom = SettingsDimension.itemPaddingVertical),
         ) {
             items(count = list.size, key = { option to list[it].record.app.packageName }) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
index 2be1d1c..99376b0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt
@@ -17,9 +17,7 @@
 package com.android.settingslib.spaprivileged.template.app
 
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -28,9 +26,8 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
-import com.android.settingslib.spa.framework.compose.stateOf
 import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction
-import com.android.settingslib.spa.widget.scaffold.SettingsScaffold
+import com.android.settingslib.spa.widget.scaffold.SearchScaffold
 import com.android.settingslib.spa.widget.ui.Spinner
 import com.android.settingslib.spaprivileged.R
 import com.android.settingslib.spaprivileged.model.app.AppListConfig
@@ -50,14 +47,12 @@
     appItem: @Composable (itemState: AppListItemModel<T>) -> Unit,
 ) {
     val showSystem = rememberSaveable { mutableStateOf(false) }
-    // TODO: Use SearchScaffold here.
-    SettingsScaffold(
+    SearchScaffold(
         title = title,
         actions = {
             ShowSystemAction(showSystem.value) { showSystem.value = it }
         },
-    ) { paddingValues ->
-        Spacer(Modifier.padding(paddingValues))
+    ) { searchQuery ->
         WorkProfilePager(primaryUserOnly) { userInfo ->
             Column(Modifier.fillMaxSize()) {
                 val options = remember { listModel.getSpinnerOptions() }
@@ -71,7 +66,7 @@
                     listModel = listModel,
                     showSystem = showSystem,
                     option = selectedOption,
-                    searchQuery = stateOf(""),
+                    searchQuery = searchQuery,
                     appItem = appItem,
                 )
             }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
index 91b852a..6641db1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -235,7 +235,7 @@
     /**
      * @return whether high quality audio is enabled or not
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
         BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
         if (bluetoothDevice == null) {
@@ -287,7 +287,7 @@
      * @param device to get codec label from
      * @return the label associated with the device codec
      */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
         BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
         int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
index 7275d6b..1745379 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java
@@ -22,6 +22,7 @@
 import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER;
 
 import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
 import android.content.Context;
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.ParentalControlsUtilsInternal;
@@ -45,6 +46,8 @@
             return new BiometricActionDisabledByAdminController(stringProvider);
         } else if (isFinancedDevice(context)) {
             return new FinancedDeviceActionDisabledByAdminController(stringProvider);
+        } else if (isSupervisedDevice(context)) {
+            return new SupervisedDeviceActionDisabledByAdminController(stringProvider, restriction);
         } else {
             return new ManagedDeviceActionDisabledByAdminController(
                     stringProvider,
@@ -54,6 +57,15 @@
         }
     }
 
+    private static boolean isSupervisedDevice(Context context) {
+        DevicePolicyManager devicePolicyManager =
+                context.getSystemService(DevicePolicyManager.class);
+        ComponentName supervisionComponent =
+                devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
+                        new UserHandle(UserHandle.myUserId()));
+        return supervisionComponent != null;
+    }
+
     /**
      * @return true if the restriction == UserManager.DISALLOW_BIOMETRIC and parental consent
      * is required.
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
index 6e93494..714accc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/BiometricActionDisabledByAdminController.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.net.Uri;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -60,6 +61,10 @@
             final Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING)
                     .putExtra(Settings.EXTRA_SUPERVISOR_RESTRICTED_SETTING_KEY,
                             Settings.SUPERVISOR_VERIFICATION_SETTING_BIOMETRICS)
+                    .setData(new Uri.Builder()
+                            .scheme("policy")
+                            .appendPath("biometric")
+                            .build())
                     .setPackage(enforcedAdmin.component.getPackageName());
             context.startActivity(intent);
         };
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java
index b83837e..7ff91f85 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/DeviceAdminStringProvider.java
@@ -79,6 +79,11 @@
     String getDisabledBiometricsParentConsentTitle();
 
     /**
+     * Returns the dialog title when the setting is blocked by supervision app.
+     */
+    String getDisabledByParentContent();
+
+    /**
      * Returns the dialog contents for when biometrics require parental consent.
      */
     String getDisabledBiometricsParentConsentContent();
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java
new file mode 100644
index 0000000..815293e9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminController.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.enterprise;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settingslib.RestrictedLockUtils;
+
+import org.jetbrains.annotations.Nullable;
+
+final class SupervisedDeviceActionDisabledByAdminController
+        extends BaseActionDisabledByAdminController {
+    private static final String TAG = "SupervisedDeviceActionDisabledByAdminController";
+    private final String mRestriction;
+
+    SupervisedDeviceActionDisabledByAdminController(
+            DeviceAdminStringProvider stringProvider, String restriction) {
+        super(stringProvider);
+        mRestriction = restriction;
+    }
+
+    @Override
+    public void setupLearnMoreButton(Context context) {
+
+    }
+
+    @Override
+    public String getAdminSupportTitle(@Nullable String restriction) {
+        return mStringProvider.getDisabledBiometricsParentConsentTitle();
+    }
+
+    @Override
+    public CharSequence getAdminSupportContentString(Context context,
+            @Nullable CharSequence supportMessage) {
+        return mStringProvider.getDisabledByParentContent();
+    }
+
+    @Nullable
+    @Override
+    public DialogInterface.OnClickListener getPositiveButtonListener(Context context,
+            RestrictedLockUtils.EnforcedAdmin enforcedAdmin) {
+        final Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING)
+                .setData(new Uri.Builder()
+                        .scheme("policy")
+                        .appendPath("user_restrictions")
+                        .appendPath(mRestriction)
+                        .build())
+                .setPackage(enforcedAdmin.component.getPackageName());
+        ComponentName resolvedSupervisionActivity =
+                intent.resolveActivity(context.getPackageManager());
+        if (resolvedSupervisionActivity == null) {
+            return null;
+        }
+        return (dialog, which) -> {
+            Log.d(TAG, "Positive button clicked, component: " + enforcedAdmin.component);
+            context.startActivity(intent);
+        };
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
index 39977df..f969a63 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/MobileNetworkTypeIconsTest.java
@@ -41,19 +41,19 @@
         MobileNetworkTypeIcon icon =
                 MobileNetworkTypeIcons.getNetworkTypeIcon(TelephonyIcons.FOUR_G);
 
-        assertThat(icon.getName()).isEqualTo(TelephonyIcons.H_PLUS.name);
+        assertThat(icon.getName()).isEqualTo(TelephonyIcons.FOUR_G.name);
         assertThat(icon.getIconResId()).isEqualTo(TelephonyIcons.ICON_4G);
     }
 
     @Test
     public void getNetworkTypeIcon_unknown_returnsUnknown() {
-        SignalIcon.MobileIconGroup unknownGroup =
-                new SignalIcon.MobileIconGroup("testUnknownNameHere", 45, 6);
+        SignalIcon.MobileIconGroup unknownGroup = new SignalIcon.MobileIconGroup(
+                "testUnknownNameHere", /* dataContentDesc= */ 45, /* dataType= */ 6);
 
         MobileNetworkTypeIcon icon = MobileNetworkTypeIcons.getNetworkTypeIcon(unknownGroup);
 
         assertThat(icon.getName()).isEqualTo("testUnknownNameHere");
-        assertThat(icon.getIconResId()).isEqualTo(45);
-        assertThat(icon.getContentDescriptionResId()).isEqualTo(6);
+        assertThat(icon.getIconResId()).isEqualTo(6);
+        assertThat(icon.getContentDescriptionResId()).isEqualTo(45);
     }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
index 99e13c3..1d5f1b2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/FakeDeviceAdminStringProvider.java
@@ -32,6 +32,7 @@
             "default_disabled_by_policy_title_financed_device";
     static final String DEFAULT_BIOMETRIC_TITLE = "biometric_title";
     static final String DEFAULT_BIOMETRIC_CONTENTS = "biometric_contents";
+    static final String DISABLED_BY_PARENT_CONTENT = "disabled_by_parent_constent";
     static final DeviceAdminStringProvider DEFAULT_DEVICE_ADMIN_STRING_PROVIDER =
             new FakeDeviceAdminStringProvider(/* url = */ null);
 
@@ -97,6 +98,11 @@
     }
 
     @Override
+    public String getDisabledByParentContent() {
+        return DISABLED_BY_PARENT_CONTENT;
+    }
+
+    @Override
     public String getDisabledBiometricsParentConsentContent() {
         return DEFAULT_BIOMETRIC_CONTENTS;
     }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java
new file mode 100644
index 0000000..5d249c7
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/SupervisedDeviceActionDisabledByAdminControllerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.enterprise;
+
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ADMIN_COMPONENT;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCED_ADMIN;
+import static com.android.settingslib.enterprise.ActionDisabledByAdminControllerTestUtils.ENFORCEMENT_ADMIN_USER_ID;
+import static com.android.settingslib.enterprise.FakeDeviceAdminStringProvider.DEFAULT_DEVICE_ADMIN_STRING_PROVIDER;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.TestCase.assertEquals;
+
+import static org.mockito.Mockito.mock;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowResolveInfo;
+
+@RunWith(RobolectricTestRunner.class)
+public class SupervisedDeviceActionDisabledByAdminControllerTest {
+
+    private Context mContext;
+
+    private ActionDisabledByAdminControllerTestUtils mTestUtils;
+    private SupervisedDeviceActionDisabledByAdminController mController;
+
+    @Before
+    public void setUp() {
+        mContext = Robolectric.buildActivity(Activity.class).setup().get();
+
+        mTestUtils = new ActionDisabledByAdminControllerTestUtils();
+
+        mController = new SupervisedDeviceActionDisabledByAdminController(
+                DEFAULT_DEVICE_ADMIN_STRING_PROVIDER, UserManager.DISALLOW_ADD_USER);
+        mController.initialize(mTestUtils.createLearnMoreButtonLauncher());
+        mController.updateEnforcedAdmin(ENFORCED_ADMIN, ENFORCEMENT_ADMIN_USER_ID);
+    }
+
+    @Test
+    public void buttonClicked() {
+        Uri restrictionUri = Uri.parse("policy:/user_restrictions/no_add_user");
+        Intent intent = new Intent(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING)
+                .setData(restrictionUri)
+                .setPackage(ADMIN_COMPONENT.getPackageName());
+        ResolveInfo resolveInfo = ShadowResolveInfo.newResolveInfo("Admin Activity",
+                ADMIN_COMPONENT.getPackageName(), "InfoActivity");
+        shadowOf(mContext.getPackageManager()).addResolveInfoForIntent(intent, resolveInfo);
+
+        DialogInterface.OnClickListener listener =
+                mController.getPositiveButtonListener(mContext, ENFORCED_ADMIN);
+        assertNotNull("Supervision controller must supply a non-null listener", listener);
+        listener.onClick(mock(DialogInterface.class), 0 /* which */);
+
+        Intent nextIntent = shadowOf(RuntimeEnvironment.application).getNextStartedActivity();
+        assertEquals(Settings.ACTION_MANAGE_SUPERVISOR_RESTRICTED_SETTING,
+                nextIntent.getAction());
+        assertEquals(restrictionUri, nextIntent.getData());
+        assertEquals(ADMIN_COMPONENT.getPackageName(), nextIntent.getPackage());
+    }
+
+    @Test
+    public void noButton() {
+        // No supervisor restricted setting Activity
+        DialogInterface.OnClickListener listener =
+                mController.getPositiveButtonListener(mContext, ENFORCED_ADMIN);
+        assertNull("Supervision controller generates null listener", listener);
+    }
+}
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 3623c78..edea3ab 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -311,4 +311,7 @@
 
     <!-- Whether tilt to bright is enabled by default. -->
     <bool name="def_wearable_tiltToBrightEnabled">false</bool>
+
+    <!-- Whether vibrate icon is shown in the status bar by default. -->
+    <integer name="def_statusBarVibrateIconEnabled">0</integer>
 </resources>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 298bbbd..2828681 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -84,6 +84,7 @@
         Settings.Global.USER_PREFERRED_RESOLUTION_HEIGHT,
         Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH,
         Settings.Global.POWER_BUTTON_LONG_PRESS,
+        Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED,
         Settings.Global.Wearable.SMART_REPLIES_ENABLED,
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME,
         Settings.Global.Wearable.CLOCKWORK_AUTO_TIME_ZONE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 9ef6d8f..e30dd2f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -329,6 +329,8 @@
         VALIDATORS.put(Global.USER_PREFERRED_REFRESH_RATE, NON_NEGATIVE_FLOAT_VALIDATOR);
         VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_HEIGHT, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_WIDTH, ANY_INTEGER_VALIDATOR);
+        VALIDATORS.put(Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED,
+                       new DiscreteValueValidator(new String[]{"0", "1"}));
         VALIDATORS.put(Global.Wearable.WET_MODE_ON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.COOLDOWN_MODE_ON, BOOLEAN_VALIDATOR);
         VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 808ea9e..6d375ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -549,7 +549,7 @@
 
         try {
             IActivityManager am = ActivityManager.getService();
-            Configuration config = am.getConfiguration();
+            final Configuration config = new Configuration();
             config.setLocales(merged);
             // indicate this isn't some passing default - the user wants this remembered
             config.userSetLocale = true;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 3a25d85..ccbfac2 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3659,7 +3659,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 210;
+            private static final int SETTINGS_VERSION = 211;
 
             private final int mUserId;
 
@@ -5531,7 +5531,17 @@
                     // removed now that feature is enabled for everyone
                     currentVersion = 210;
                 }
-
+                if (currentVersion == 210) {
+                    final SettingsState secureSettings = getSecureSettingsLocked(userId);
+                    final int defaultValueVibrateIconEnabled = getContext().getResources()
+                            .getInteger(R.integer.def_statusBarVibrateIconEnabled);
+                    secureSettings.insertSettingOverrideableByRestoreLocked(
+                            Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
+                            String.valueOf(defaultValueVibrateIconEnabled),
+                            null /* tag */, true /* makeDefault */,
+                            SettingsState.SYSTEM_PACKAGE_NAME);
+                    currentVersion = 211;
+                }
                 // vXXX: Add new settings above this point.
 
                 if (currentVersion != newVersion) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 765ee89..c388826 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -42,8 +42,6 @@
 import android.util.Base64;
 import android.util.Slog;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -51,6 +49,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
@@ -1087,6 +1087,7 @@
             parseStateLocked(parser);
             return true;
         } catch (XmlPullParserException | IOException e) {
+            Slog.e(LOG_TAG, "parse settings xml failed", e);
             return false;
         } finally {
             IoUtils.closeQuietly(in);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 9747a6c..aea2f52 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -817,7 +817,8 @@
                  Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
                  Settings.Secure.ACCESSIBILITY_SHOW_WINDOW_MAGNIFICATION_PROMPT,
                  Settings.Secure.ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
-                 Settings.Secure.UI_TRANSLATION_ENABLED);
+                 Settings.Secure.UI_TRANSLATION_ENABLED,
+                 Settings.Secure.CREDENTIAL_SERVICE);
 
     @Test
     public void systemSettingsBackedUpOrDenied() {
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 4ed28d5..55160fb 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -17,9 +17,10 @@
 
 import android.os.Looper;
 import android.test.AndroidTestCase;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlSerializer;
+
 import com.google.common.base.Strings;
 
 import java.io.ByteArrayOutputStream;
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index fecf124..90fab08 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -154,6 +154,7 @@
     <uses-permission android:name="android.permission.CONTROL_UI_TRACING" />
     <uses-permission android:name="android.permission.SIGNAL_PERSISTENT_PROCESSES" />
     <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
+    <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
     <!-- Internal permissions granted to the shell. -->
     <uses-permission android:name="android.permission.FORCE_BACK" />
     <uses-permission android:name="android.permission.BATTERY_STATS" />
@@ -210,6 +211,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.CREATE_USERS" />
+    <uses-permission android:name="android.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION" />
     <uses-permission android:name="android.permission.QUERY_USERS" />
     <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" />
     <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
@@ -715,6 +717,9 @@
     <!-- Permission required for CTS test - ActivityPermissionRationaleTest -->
     <uses-permission android:name="android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY" />
 
+    <!-- Permission required for CTS test - CtsDeviceLockTestCases -->
+    <uses-permission android:name="android.permission.MANAGE_DEVICE_LOCK_STATE" />
+
     <application android:label="@string/app_label"
                 android:theme="@android:style/Theme.DeviceDefault.DayNight"
                 android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index ca36fa4..fdfad2b 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -25,7 +25,6 @@
 import android.os.Looper
 import android.util.Log
 import android.util.MathUtils
-import android.view.GhostView
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
@@ -86,6 +85,9 @@
          */
         val sourceIdentity: Any
 
+        /** The CUJ associated to this controller. */
+        val cuj: DialogCuj?
+
         /**
          * Move the drawing of the source in the overlay of [viewGroup].
          *
@@ -142,7 +144,31 @@
          * controlled by this controller.
          */
         // TODO(b/252723237): Make this non-nullable
-        fun jankConfigurationBuilder(cuj: Int): InteractionJankMonitor.Configuration.Builder?
+        fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder?
+
+        companion object {
+            /**
+             * Create a [Controller] that can animate [source] to and from a dialog.
+             *
+             * Important: The view must be attached to a [ViewGroup] when calling this function and
+             * during the animation. For safety, this method will return null when it is not.
+             *
+             * Note: The background of [view] should be a (rounded) rectangle so that it can be
+             * properly animated.
+             */
+            fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
+                if (source.parent !is ViewGroup) {
+                    Log.e(
+                        TAG,
+                        "Skipping animation as view $source is not attached to a ViewGroup",
+                        Exception(),
+                    )
+                    return null
+                }
+
+                return ViewDialogLaunchAnimatorController(source, cuj)
+            }
+        }
     }
 
     /**
@@ -172,7 +198,12 @@
         cuj: DialogCuj? = null,
         animateBackgroundBoundsChange: Boolean = false
     ) {
-        show(dialog, createController(view), cuj, animateBackgroundBoundsChange)
+        val controller = Controller.fromView(view, cuj)
+        if (controller == null) {
+            dialog.show()
+        } else {
+            show(dialog, controller, animateBackgroundBoundsChange)
+        }
     }
 
     /**
@@ -187,10 +218,10 @@
      * Caveats: When calling this function and [dialog] is not a fullscreen dialog, then it will be
      * made fullscreen and 2 views will be inserted between the dialog DecorView and its children.
      */
+    @JvmOverloads
     fun show(
         dialog: Dialog,
         controller: Controller,
-        cuj: DialogCuj? = null,
         animateBackgroundBoundsChange: Boolean = false
     ) {
         if (Looper.myLooper() != Looper.getMainLooper()) {
@@ -207,7 +238,10 @@
                 it.dialog.window.decorView.viewRootImpl == controller.viewRoot
             }
         val animateFrom =
-            animatedParent?.dialogContentWithBackground?.let { createController(it) } ?: controller
+            animatedParent?.dialogContentWithBackground?.let {
+                Controller.fromView(it, controller.cuj)
+            }
+                ?: controller
 
         if (animatedParent == null && animateFrom !is LaunchableView) {
             // Make sure the View we launch from implements LaunchableView to avoid visibility
@@ -244,96 +278,12 @@
                 animateBackgroundBoundsChange,
                 animatedParent,
                 isForTesting,
-                cuj,
             )
 
         openedDialogs.add(animatedDialog)
         animatedDialog.start()
     }
 
-    /** Create a [Controller] that can animate [source] to & from a dialog. */
-    private fun createController(source: View): Controller {
-        return object : Controller {
-            override val viewRoot: ViewRootImpl
-                get() = source.viewRootImpl
-
-            override val sourceIdentity: Any = source
-
-            override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
-                // Create a temporary ghost of the source (which will make it invisible) and add it
-                // to the host dialog.
-                GhostView.addGhost(source, viewGroup)
-
-                // The ghost of the source was just created, so the source is currently invisible.
-                // We need to make sure that it stays invisible as long as the dialog is shown or
-                // animating.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
-            }
-
-            override fun stopDrawingInOverlay() {
-                // Note: here we should remove the ghost from the overlay, but in practice this is
-                // already done by the launch controllers created below.
-
-                // Make sure we allow the source to change its visibility again.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-                source.visibility = View.VISIBLE
-            }
-
-            override fun createLaunchController(): LaunchAnimator.Controller {
-                val delegate = GhostedViewLaunchAnimatorController(source)
-                return object : LaunchAnimator.Controller by delegate {
-                    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
-                        // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
-                        // ghost (that ghosts only the source content, and not its background) will
-                        // be added right after this by the delegate and will be animated.
-                        GhostView.removeGhost(source)
-                        delegate.onLaunchAnimationStart(isExpandingFullyAbove)
-                    }
-
-                    override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
-                        delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
-
-                        // We hide the source when the dialog is showing. We will make this view
-                        // visible again when dismissing the dialog. This does nothing if the source
-                        // implements [LaunchableView], as it's already INVISIBLE in that case.
-                        source.visibility = View.INVISIBLE
-                    }
-                }
-            }
-
-            override fun createExitController(): LaunchAnimator.Controller {
-                return GhostedViewLaunchAnimatorController(source)
-            }
-
-            override fun shouldAnimateExit(): Boolean {
-                // The source should be invisible by now, if it's not then something else changed
-                // its visibility and we probably don't want to run the animation.
-                if (source.visibility != View.INVISIBLE) {
-                    return false
-                }
-
-                return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
-            }
-
-            override fun onExitAnimationCancelled() {
-                // Make sure we allow the source to change its visibility again.
-                (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
-
-                // If the view is invisible it's probably because of us, so we make it visible
-                // again.
-                if (source.visibility == View.INVISIBLE) {
-                    source.visibility = View.VISIBLE
-                }
-            }
-
-            override fun jankConfigurationBuilder(
-                cuj: Int
-            ): InteractionJankMonitor.Configuration.Builder? {
-                return InteractionJankMonitor.Configuration.Builder.withView(cuj, source)
-            }
-        }
-    }
-
     /**
      * Launch [dialog] from [another dialog][animateFrom] that was shown using [show]. This will
      * allow for dismissing the whole stack.
@@ -563,9 +513,6 @@
      * Whether synchronization should be disabled, which can be useful if we are running in a test.
      */
     private val forceDisableSynchronization: Boolean,
-
-    /** Interaction to which the dialog animation is associated. */
-    private val cuj: DialogCuj? = null
 ) {
     /**
      * The DecorView of this dialog window.
@@ -618,8 +565,9 @@
     private var hasInstrumentedJank = false
 
     fun start() {
+        val cuj = controller.cuj
         if (cuj != null) {
-            val config = controller.jankConfigurationBuilder(cuj.cujType)
+            val config = controller.jankConfigurationBuilder()
             if (config != null) {
                 if (cuj.tag != null) {
                     config.setTag(cuj.tag)
@@ -917,7 +865,7 @@
                 }
 
                 if (hasInstrumentedJank) {
-                    interactionJankMonitor.end(cuj!!.cujType)
+                    interactionJankMonitor.end(controller.cuj!!.cujType)
                 }
             }
         )
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
index 8ce372d..40a5e97 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Expandable.kt
@@ -30,7 +30,12 @@
      */
     fun activityLaunchController(cujType: Int? = null): ActivityLaunchAnimator.Controller?
 
-    // TODO(b/230830644): Introduce DialogLaunchAnimator and a function to expose it here.
+    /**
+     * Create a [DialogLaunchAnimator.Controller] that can be used to expand this [Expandable] into
+     * a Dialog, or return `null` if this [Expandable] should not be animated (e.g. if it is
+     * currently not attached or visible).
+     */
+    fun dialogLaunchController(cuj: DialogCuj? = null): DialogLaunchAnimator.Controller?
 
     companion object {
         /**
@@ -39,6 +44,7 @@
          * Note: The background of [view] should be a (rounded) rectangle so that it can be properly
          * animated.
          */
+        @JvmStatic
         fun fromView(view: View): Expandable {
             return object : Expandable {
                 override fun activityLaunchController(
@@ -46,6 +52,12 @@
                 ): ActivityLaunchAnimator.Controller? {
                     return ActivityLaunchAnimator.Controller.fromView(view, cujType)
                 }
+
+                override fun dialogLaunchController(
+                    cuj: DialogCuj?
+                ): DialogLaunchAnimator.Controller? {
+                    return DialogLaunchAnimator.Controller.fromView(view, cuj)
+                }
             }
         }
     }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
new file mode 100644
index 0000000..ecee598
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewDialogLaunchAnimatorController.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation
+
+import android.view.GhostView
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewRootImpl
+import com.android.internal.jank.InteractionJankMonitor
+
+/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
+class ViewDialogLaunchAnimatorController
+internal constructor(
+    private val source: View,
+    override val cuj: DialogCuj?,
+) : DialogLaunchAnimator.Controller {
+    override val viewRoot: ViewRootImpl
+        get() = source.viewRootImpl
+
+    override val sourceIdentity: Any = source
+
+    override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
+        // Create a temporary ghost of the source (which will make it invisible) and add it
+        // to the host dialog.
+        GhostView.addGhost(source, viewGroup)
+
+        // The ghost of the source was just created, so the source is currently invisible.
+        // We need to make sure that it stays invisible as long as the dialog is shown or
+        // animating.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
+    }
+
+    override fun stopDrawingInOverlay() {
+        // Note: here we should remove the ghost from the overlay, but in practice this is
+        // already done by the launch controllers created below.
+
+        // Make sure we allow the source to change its visibility again.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+        source.visibility = View.VISIBLE
+    }
+
+    override fun createLaunchController(): LaunchAnimator.Controller {
+        val delegate = GhostedViewLaunchAnimatorController(source)
+        return object : LaunchAnimator.Controller by delegate {
+            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
+                // Remove the temporary ghost added by [startDrawingInOverlayOf]. Another
+                // ghost (that ghosts only the source content, and not its background) will
+                // be added right after this by the delegate and will be animated.
+                GhostView.removeGhost(source)
+                delegate.onLaunchAnimationStart(isExpandingFullyAbove)
+            }
+
+            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
+                delegate.onLaunchAnimationEnd(isExpandingFullyAbove)
+
+                // We hide the source when the dialog is showing. We will make this view
+                // visible again when dismissing the dialog. This does nothing if the source
+                // implements [LaunchableView], as it's already INVISIBLE in that case.
+                source.visibility = View.INVISIBLE
+            }
+        }
+    }
+
+    override fun createExitController(): LaunchAnimator.Controller {
+        return GhostedViewLaunchAnimatorController(source)
+    }
+
+    override fun shouldAnimateExit(): Boolean {
+        // The source should be invisible by now, if it's not then something else changed
+        // its visibility and we probably don't want to run the animation.
+        if (source.visibility != View.INVISIBLE) {
+            return false
+        }
+
+        return source.isAttachedToWindow && ((source.parent as? View)?.isShown ?: true)
+    }
+
+    override fun onExitAnimationCancelled() {
+        // Make sure we allow the source to change its visibility again.
+        (source as? LaunchableView)?.setShouldBlockVisibilityChanges(false)
+
+        // If the view is invisible it's probably because of us, so we make it visible
+        // again.
+        if (source.visibility == View.INVISIBLE) {
+            source.visibility = View.VISIBLE
+        }
+    }
+
+    override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
+        val type = cuj?.cujType ?: return null
+        return InteractionJankMonitor.Configuration.Builder.withView(type, source)
+    }
+}
diff --git a/packages/SystemUI/checks/Android.bp b/packages/SystemUI/checks/Android.bp
index 9671add..40580d2 100644
--- a/packages/SystemUI/checks/Android.bp
+++ b/packages/SystemUI/checks/Android.bp
@@ -47,6 +47,10 @@
         "tests/**/*.kt",
         "tests/**/*.java",
     ],
+    data: [
+        ":framework",
+        ":androidx.annotation_annotation",
+    ],
     static_libs: [
         "SystemUILintChecker",
         "junit",
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
index 4eeeb85..4b9aa13 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
@@ -32,7 +32,8 @@
 class SoftwareBitmapDetector : Detector(), SourceCodeScanner {
 
     override fun getApplicableReferenceNames(): List<String> {
-        return mutableListOf("ALPHA_8", "RGB_565", "ARGB_8888", "RGBA_F16", "RGBA_1010102")
+        return mutableListOf(
+            "ALPHA_8", "RGB_565", "ARGB_4444", "ARGB_8888", "RGBA_F16", "RGBA_1010102")
     }
 
     override fun visitReference(
@@ -40,13 +41,12 @@
             reference: UReferenceExpression,
             referenced: PsiElement
     ) {
-
         val evaluator = context.evaluator
         if (evaluator.isMemberInClass(referenced as? PsiField, "android.graphics.Bitmap.Config")) {
             context.report(
                     ISSUE,
                     referenced,
-                    context.getNameLocation(referenced),
+                    context.getNameLocation(reference),
                     "Replace software bitmap with `Config.HARDWARE`"
             )
         }
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
new file mode 100644
index 0000000..1db0725
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/StaticSettingsProviderDetector.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+private const val CLASS_SETTINGS = "android.provider.Settings"
+
+/**
+ * Detects usage of static methods in android.provider.Settings and suggests to use an injected
+ * settings provider instance instead.
+ */
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> {
+        return listOf(
+            "getFloat",
+            "getInt",
+            "getLong",
+            "getString",
+            "getUriFor",
+            "putFloat",
+            "putInt",
+            "putLong",
+            "putString"
+        )
+    }
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val evaluator = context.evaluator
+        val className = method.containingClass?.qualifiedName
+        if (
+            className != "$CLASS_SETTINGS.Global" &&
+                className != "$CLASS_SETTINGS.Secure" &&
+                className != "$CLASS_SETTINGS.System"
+        ) {
+            return
+        }
+        if (!evaluator.isStatic(method)) {
+            return
+        }
+
+        val subclassName = className.substring(CLASS_SETTINGS.length + 1)
+
+        context.report(
+            ISSUE,
+            method,
+            context.getNameLocation(node),
+            "`@Inject` a ${subclassName}Settings instead"
+        )
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE: Issue =
+            Issue.create(
+                id = "StaticSettingsProvider",
+                briefDescription = "Static settings provider usage",
+                explanation =
+                    """
+                    Static settings provider methods, such as `Settings.Global.putInt()`, should \
+                    not be used because they make testing difficult. Instead, use an injected \
+                    settings provider. For example, instead of calling `Settings.Secure.getInt()`, \
+                    annotate the class constructor with `@Inject` and add `SecureSettings` to the \
+                    parameters.
+                    """,
+                category = Category.CORRECTNESS,
+                priority = 8,
+                severity = Severity.WARNING,
+                implementation =
+                    Implementation(
+                        StaticSettingsProviderDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                    )
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index cf7c1b5..3f334c1c 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -36,6 +36,7 @@
                 RegisterReceiverViaContextDetector.ISSUE,
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
+                StaticSettingsProviderDetector.ISSUE
         )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
index 486af9d..141dd05 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/AndroidStubs.kt
@@ -18,6 +18,8 @@
 
 import com.android.annotations.NonNull
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.TestFiles.LibraryReferenceTestFile
+import java.io.File
 import org.intellij.lang.annotations.Language
 
 @Suppress("UnstableApiUsage")
@@ -30,132 +32,8 @@
  */
 internal val androidStubs =
     arrayOf(
-        indentedJava(
-            """
-package android.app;
-
-public class ActivityManager {
-    public static int getCurrentUser() {}
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.accounts;
-
-public class AccountManager {
-    public static AccountManager get(Context context) { return null; }
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.os;
-import android.content.pm.UserInfo;
-import android.annotation.UserIdInt;
-
-public class UserManager {
-    public UserInfo getUserInfo(@UserIdInt int userId) {}
-}
-"""
-        ),
-        indentedJava("""
-package android.annotation;
-
-public @interface UserIdInt {}
-"""),
-        indentedJava("""
-package android.content.pm;
-
-public class UserInfo {}
-"""),
-        indentedJava("""
-package android.os;
-
-public class Looper {}
-"""),
-        indentedJava("""
-package android.os;
-
-public class Handler {}
-"""),
-        indentedJava("""
-package android.content;
-
-public class ServiceConnection {}
-"""),
-        indentedJava("""
-package android.os;
-
-public enum UserHandle {
-    ALL
-}
-"""),
-        indentedJava(
-            """
-package android.content;
-import android.os.UserHandle;
-import android.os.Handler;
-import android.os.Looper;
-import java.util.concurrent.Executor;
-
-public class Context {
-    public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {}
-    public void registerReceiverAsUser(
-            BroadcastReceiver receiver, UserHandle user, IntentFilter filter,
-            String broadcastPermission, Handler scheduler) {}
-    public void registerReceiverForAllUsers(
-            BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission,
-            Handler scheduler) {}
-    public void sendBroadcast(Intent intent) {}
-    public void sendBroadcast(Intent intent, String receiverPermission) {}
-    public void sendBroadcastAsUser(Intent intent, UserHandle userHandle, String permission) {}
-    public void bindService(Intent intent) {}
-    public void bindServiceAsUser(
-            Intent intent, ServiceConnection connection, int flags, UserHandle userHandle) {}
-    public void unbindService(ServiceConnection connection) {}
-    public Looper getMainLooper() { return null; }
-    public Executor getMainExecutor() { return null; }
-    public Handler getMainThreadHandler() { return null; }
-    public final @Nullable <T> T getSystemService(@NonNull Class<T> serviceClass) { return null; }
-    public abstract @Nullable Object getSystemService(@ServiceName @NonNull String name);
-}
-"""
-        ),
-        indentedJava(
-            """
-package android.app;
-import android.content.Context;
-
-public class Activity extends Context {}
-"""
-        ),
-        indentedJava(
-            """
-package android.graphics;
-
-public class Bitmap {
-    public enum Config {
-        ARGB_8888,
-        RGB_565,
-        HARDWARE
-    }
-    public static Bitmap createBitmap(int width, int height, Config config) {
-        return null;
-    }
-}
-"""
-        ),
-        indentedJava("""
-package android.content;
-
-public class BroadcastReceiver {}
-"""),
-        indentedJava("""
-package android.content;
-
-public class IntentFilter {}
-"""),
+        LibraryReferenceTestFile(File("framework.jar").canonicalFile),
+        LibraryReferenceTestFile(File("androidx.annotation_annotation.jar").canonicalFile),
         indentedJava(
             """
 package com.android.systemui.settings;
@@ -167,23 +45,4 @@
 }
 """
         ),
-        indentedJava(
-            """
-package androidx.annotation;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-import static java.lang.annotation.ElementType.CONSTRUCTOR;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-@Retention(SOURCE)
-@Target({METHOD,CONSTRUCTOR,TYPE,PARAMETER})
-public @interface WorkerThread {
-}
-"""
-        ),
     )
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
index 6ae8fd3..c35ac61 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BindServiceOnMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BindServiceOnMainThreadDetectorTest : LintDetectorTest() {
+class BindServiceOnMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BindServiceOnMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BindServiceOnMainThreadDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
index 7d42280..376acb5 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/BroadcastSentViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class BroadcastSentViaContextDetectorTest : LintDetectorTest() {
+class BroadcastSentViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = BroadcastSentViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(BroadcastSentViaContextDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
index c468af8..301c338 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedMainThreadDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedMainThreadDetectorTest : LintDetectorTest() {
+class NonInjectedMainThreadDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedMainThreadDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(NonInjectedMainThreadDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
index c83a35b..0a74bfc 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/NonInjectedServiceDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class NonInjectedServiceDetectorTest : LintDetectorTest() {
+class NonInjectedServiceDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = NonInjectedServiceDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
     override fun getIssues(): List<Issue> = listOf(NonInjectedServiceDetector.ISSUE)
 
     @Test
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
index ebcddeb..9ed7aa0 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterReceiverViaContextDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class RegisterReceiverViaContextDetectorTest : LintDetectorTest() {
+class RegisterReceiverViaContextDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = RegisterReceiverViaContextDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(RegisterReceiverViaContextDetector.ISSUE)
 
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
index b03a11c..54cac7b 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SlowUserQueryDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SlowUserQueryDetectorTest : LintDetectorTest() {
+class SlowUserQueryDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SlowUserQueryDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> =
         listOf(
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
index fb6537e..c632636 100644
--- a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SoftwareBitmapDetectorTest.kt
@@ -16,18 +16,15 @@
 
 package com.android.internal.systemui.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
 import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintTask
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
 
 @Suppress("UnstableApiUsage")
-class SoftwareBitmapDetectorTest : LintDetectorTest() {
+class SoftwareBitmapDetectorTest : SystemUILintDetectorTest() {
 
     override fun getDetector(): Detector = SoftwareBitmapDetector()
-    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
 
     override fun getIssues(): List<Issue> = listOf(SoftwareBitmapDetector.ISSUE)
 
@@ -54,12 +51,12 @@
             .run()
             .expect(
                 """
-                src/android/graphics/Bitmap.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
-                        ARGB_8888,
-                        ~~~~~~~~~
-                src/android/graphics/Bitmap.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
-                        RGB_565,
-                        ~~~~~~~
+                src/TestClass.java:5: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                      Bitmap.createBitmap(300, 300, Bitmap.Config.RGB_565);
+                                                                  ~~~~~~~
+                src/TestClass.java:6: Warning: Replace software bitmap with Config.HARDWARE [SoftwareBitmap]
+                      Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888);
+                                                                  ~~~~~~~~~
                 0 errors, 2 warnings
                 """
             )
@@ -70,7 +67,7 @@
         lint()
             .files(
                 TestFiles.java(
-                        """
+                    """
                     import android.graphics.Bitmap;
 
                     public class TestClass {
@@ -79,8 +76,7 @@
                         }
                     }
                 """
-                    )
-                    .indented(),
+                ),
                 *stubs
             )
             .issues(SoftwareBitmapDetector.ISSUE)
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
new file mode 100644
index 0000000..b83ed70
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/StaticSettingsProviderDetectorTest.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class StaticSettingsProviderDetectorTest : SystemUILintDetectorTest() {
+
+    override fun getDetector(): Detector = StaticSettingsProviderDetector()
+    override fun getIssues(): List<Issue> = listOf(StaticSettingsProviderDetector.ISSUE)
+
+    @Test
+    fun testGetServiceWithString() {
+        lint()
+            .files(
+                TestFiles.java(
+                        """
+                        package test.pkg;
+
+                        import android.provider.Settings;
+                        import android.provider.Settings.Global;
+                        import android.provider.Settings.Secure;
+
+                        public class TestClass {
+                            public void getSystemServiceWithoutDagger(Context context) {
+                                final ContentResolver cr = mContext.getContentResolver();
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                                Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                                Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                                Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                                Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                                Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                                Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                                Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                                Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                                Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                            }
+                        }
+                        """
+                    )
+                    .indented(),
+                *stubs
+            )
+            .issues(StaticSettingsProviderDetector.ISSUE)
+            .run()
+            .expect(
+                """
+                src/test/pkg/TestClass.java:10: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:11: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:12: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:13: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:14: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:15: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:16: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:17: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.getString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:18: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putFloat(cr, Settings.Global.UNLOCK_SOUND, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:19: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putInt(cr, Settings.Global.UNLOCK_SOUND, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:20: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putLong(cr, Settings.Global.UNLOCK_SOUND, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:21: Warning: @Inject a GlobalSettings instead [StaticSettingsProvider]
+                        Global.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:23: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:24: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:25: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:26: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED);
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:27: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:28: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:29: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:30: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.getString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:31: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putFloat(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1f);
+                               ~~~~~~~~
+                src/test/pkg/TestClass.java:32: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putInt(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1);
+                               ~~~~~~
+                src/test/pkg/TestClass.java:33: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putLong(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, 1L);
+                               ~~~~~~~
+                src/test/pkg/TestClass.java:34: Warning: @Inject a SecureSettings instead [StaticSettingsProvider]
+                        Secure.putString(cr, Settings.Secure.ASSIST_GESTURE_ENABLED, "1");
+                               ~~~~~~~~~
+                src/test/pkg/TestClass.java:36: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:37: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:38: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:39: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT);
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:40: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:41: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:42: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:43: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.getString(cr, Settings.System.SCREEN_OFF_TIMEOUT, "1");
+                                        ~~~~~~~~~
+                src/test/pkg/TestClass.java:44: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putFloat(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1f);
+                                        ~~~~~~~~
+                src/test/pkg/TestClass.java:45: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putInt(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1);
+                                        ~~~~~~
+                src/test/pkg/TestClass.java:46: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putLong(cr, Settings.System.SCREEN_OFF_TIMEOUT, 1L);
+                                        ~~~~~~~
+                src/test/pkg/TestClass.java:47: Warning: @Inject a SystemSettings instead [StaticSettingsProvider]
+                        Settings.System.putString(cr, Settings.Global.UNLOCK_SOUND, "1");
+                                        ~~~~~~~~~
+                0 errors, 36 warnings
+                """
+            )
+    }
+
+    private val stubs = androidStubs
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
new file mode 100644
index 0000000..3f93f07
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/SystemUILintDetectorTest.kt
@@ -0,0 +1,48 @@
+package com.android.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+import org.junit.ClassRule
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.model.Statement
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+abstract class SystemUILintDetectorTest : LintDetectorTest() {
+
+    companion object {
+        @ClassRule
+        @JvmField
+        val libraryChecker: LibraryExists =
+            LibraryExists("framework.jar", "androidx.annotation_annotation.jar")
+    }
+
+    class LibraryExists(vararg val libraryNames: String) : TestRule {
+        override fun apply(base: Statement, description: Description): Statement {
+            return object : Statement() {
+                override fun evaluate() {
+                    for (libName in libraryNames) {
+                        val libFile = File(libName)
+                        if (!libFile.canonicalFile.exists()) {
+                            throw Exception(
+                                "Could not find $libName in the test's working directory. " +
+                                    "File ${libFile.absolutePath} does not exist."
+                            )
+                        }
+                    }
+                    base.evaluate()
+                }
+            }
+        }
+    }
+    /**
+     * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+     * in Android Studio has the same result as running the tests in atest
+     */
+    override fun lint(): TestLintTask =
+        super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
index 065c314..50c3d7e 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/ExpandableController.kt
@@ -40,17 +40,16 @@
 import androidx.compose.ui.unit.LayoutDirection
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.LaunchAnimator
 import kotlin.math.roundToInt
 
-/** A controller that can control animated launches. */
+/** A controller that can control animated launches from an [Expandable]. */
 interface ExpandableController {
-    /** Create an [ActivityLaunchAnimator.Controller] to animate into an Activity. */
-    fun forActivity(): ActivityLaunchAnimator.Controller
-
-    /** Create a [DialogLaunchAnimator.Controller] to animate into a Dialog. */
-    fun forDialog(): DialogLaunchAnimator.Controller
+    /** The [Expandable] controlled by this controller. */
+    val expandable: Expandable
 }
 
 /**
@@ -120,13 +119,26 @@
     private val layoutDirection: LayoutDirection,
     private val isComposed: State<Boolean>,
 ) : ExpandableController {
-    override fun forActivity(): ActivityLaunchAnimator.Controller {
-        return activityController()
-    }
+    override val expandable: Expandable =
+        object : Expandable {
+            override fun activityLaunchController(
+                cujType: Int?,
+            ): ActivityLaunchAnimator.Controller? {
+                if (!isComposed.value) {
+                    return null
+                }
 
-    override fun forDialog(): DialogLaunchAnimator.Controller {
-        return dialogController()
-    }
+                return activityController(cujType)
+            }
+
+            override fun dialogLaunchController(cuj: DialogCuj?): DialogLaunchAnimator.Controller? {
+                if (!isComposed.value) {
+                    return null
+                }
+
+                return dialogController(cuj)
+            }
+        }
 
     /**
      * Create a [LaunchAnimator.Controller] that is going to be used to drive an activity or dialog
@@ -233,7 +245,7 @@
     }
 
     /** Create an [ActivityLaunchAnimator.Controller] that can be used to animate activities. */
-    private fun activityController(): ActivityLaunchAnimator.Controller {
+    private fun activityController(cujType: Int?): ActivityLaunchAnimator.Controller {
         val delegate = launchController()
         return object : ActivityLaunchAnimator.Controller, LaunchAnimator.Controller by delegate {
             override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
@@ -248,10 +260,11 @@
         }
     }
 
-    private fun dialogController(): DialogLaunchAnimator.Controller {
+    private fun dialogController(cuj: DialogCuj?): DialogLaunchAnimator.Controller {
         return object : DialogLaunchAnimator.Controller {
             override val viewRoot: ViewRootImpl = composeViewRoot.viewRootImpl
             override val sourceIdentity: Any = this@ExpandableControllerImpl
+            override val cuj: DialogCuj? = cuj
 
             override fun startDrawingInOverlayOf(viewGroup: ViewGroup) {
                 val newOverlay = viewGroup.overlay as ViewGroupOverlay
@@ -294,9 +307,7 @@
                 isDialogShowing.value = false
             }
 
-            override fun jankConfigurationBuilder(
-                cuj: Int
-            ): InteractionJankMonitor.Configuration.Builder? {
+            override fun jankConfigurationBuilder(): InteractionJankMonitor.Configuration.Builder? {
                 // TODO(b/252723237): Add support for jank monitoring when animating from a
                 // Composable.
                 return null
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index d0d3052..0f037e4 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -189,45 +189,8 @@
 -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTracker.kt
 -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
 -packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerProd.kt
--packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
--packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
--packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
--packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
--packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
--packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
--packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
--packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
--packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
--packages/SystemUI/src/com/android/systemui/media/MediaData.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
--packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
--packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
--packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
--packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
--packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
--packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
 -packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
 -packages/SystemUI/src/com/android/systemui/media/MediaProjectionCaptureTarget.kt
--packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
--packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
--packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
--packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
--packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
--packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
--packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
--packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
--packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
--packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
--packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
--packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
--packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
--packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
--packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
 -packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
 -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
 -packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -653,26 +616,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/lifecycle/InstantTaskExecutorRule.kt
 -packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
--packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt
@@ -832,7 +775,6 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/FoldStateLoggingProviderTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldLatencyTrackerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/unfold/UnfoldTransitionWallpaperControllerTest.kt
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index b3dd955..dee0f5c 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -205,6 +205,13 @@
             n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)),
             n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666))
     )),
+    MONOCHROMATIC(CoreSpec(
+            a1 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            a2 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            a3 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            n1 = TonalSpec(HueSource(), ChromaConstant(.0)),
+            n2 = TonalSpec(HueSource(), ChromaConstant(.0))
+    )),
 }
 
 class ColorScheme(
@@ -219,7 +226,7 @@
     val neutral1: List<Int>
     val neutral2: List<Int>
 
-    constructor(@ColorInt seed: Int, darkTheme: Boolean):
+    constructor(@ColorInt seed: Int, darkTheme: Boolean) :
             this(seed, darkTheme, Style.TONAL_SPOT)
 
     @JvmOverloads
@@ -227,7 +234,7 @@
         wallpaperColors: WallpaperColors,
         darkTheme: Boolean,
         style: Style = Style.TONAL_SPOT
-    ):
+    ) :
             this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
 
     val allAccentColors: List<Int>
@@ -472,4 +479,4 @@
             return huePopulation
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index dabb43b..89f5c2c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -18,6 +18,7 @@
 import android.graphics.drawable.Drawable
 import android.view.View
 import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -70,6 +71,9 @@
 
     /** Optional method for dumping debug information */
     fun dump(pw: PrintWriter) { }
+
+    /** Optional method for debug logging */
+    fun setLogBuffer(logBuffer: LogBuffer) { }
 }
 
 /** Interface for a specific clock face version rendered by the clock */
diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
similarity index 71%
copy from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
copy to packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
index 9e61236..de0e526 100644
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ b/packages/SystemUI/res-keyguard/drawable/fullscreen_userswitcher_menu_item_divider.xml
@@ -12,6 +12,9 @@
   ~ 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.
+  ~ limitations under the License
   -->
-<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+    <size android:height="@dimen/bouncer_user_switcher_popup_items_divider_height"/>
+    <solid android:color="@color/user_switcher_fullscreen_bg"/>
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
index 16a1d94..647abee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
@@ -27,6 +27,7 @@
     systemui:layout_constraintEnd_toEndOf="parent"
     systemui:layout_constraintTop_toTopOf="parent"
     android:layout_marginHorizontal="@dimen/status_view_margin_horizontal"
+    android:clipChildren="false"
     android:layout_width="0dp"
     android:layout_height="wrap_content">
     <LinearLayout
diff --git a/packages/SystemUI/res-keyguard/values-af/strings.xml b/packages/SystemUI/res-keyguard/values-af/strings.xml
index d5e84f9..d5552f6 100644
--- a/packages/SystemUI/res-keyguard/values-af/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-af/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon word vereis nadat toestel herbegin het"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN word vereis nadat toestel herbegin het"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wagwoord word vereis nadat toestel herbegin het"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon word vir bykomende sekuriteit vereis"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN word vir bykomende sekuriteit vereis"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wagwoord word vir bykomende sekuriteit vereis"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Toestel is deur administrateur gesluit"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Toestel is handmatig gesluit"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie herken nie"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Verstek"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Borrel"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-am/strings.xml b/packages/SystemUI/res-keyguard/values-am/strings.xml
index be52c44..533e5a2 100644
--- a/packages/SystemUI/res-keyguard/values-am/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-am/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"መሣሪያ ዳግም ከጀመረ በኋላ ሥርዓተ ጥለት ያስፈልጋል"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"መሣሪያ ዳግም ከተነሳ በኋላ ፒን ያስፈልጋል"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"መሣሪያ ዳግም ከጀመረ በኋላ የይለፍ ቃል ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ሥርዓተ ጥለት ለተጨማሪ ደህንነት ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ፒን ለተጨማሪ ደህንነት ያስፈልጋል"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"የይለፍ ቃል ለተጨማሪ ደህንነት ያስፈልጋል"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"መሣሪያ በአስተዳዳሪ ተቆልፏል"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"መሣሪያ በተጠቃሚው ራሱ ተቆልፏል"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"አልታወቀም"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ነባሪ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"አረፋ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"አናሎግ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ar/strings.xml b/packages/SystemUI/res-keyguard/values-ar/strings.xml
index adb57b6..81ce7d3 100644
--- a/packages/SystemUI/res-keyguard/values-ar/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ar/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"يجب رسم النقش بعد إعادة تشغيل الجهاز"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"يجب إدخال رقم التعريف الشخصي بعد إعادة تشغيل الجهاز"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"يجب إدخال كلمة المرور بعد إعادة تشغيل الجهاز"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"يجب رسم النقش لمزيد من الأمان"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"يجب إدخال رقم التعريف الشخصي لمزيد من الأمان"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"يجب إدخال كلمة المرور لمزيد من الأمان"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"اختار المشرف قفل الجهاز"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"تم حظر الجهاز يدويًا"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"لم يتم التعرّف عليه."</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"تلقائي"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"فقاعة"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ساعة تقليدية"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-as/strings.xml b/packages/SystemUI/res-keyguard/values-as/strings.xml
index cbfb325..443f666 100644
--- a/packages/SystemUI/res-keyguard/values-as/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-as/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত আৰ্হি দিয়াটো বাধ্যতামূলক"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পিন দিয়াটো বাধ্যতামূলক"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইচ ৰিষ্টাৰ্ট হোৱাৰ পাছত পাছৱৰ্ড দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিৰিক্ত সুৰক্ষাৰ বাবে আর্হি দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিৰিক্ত সুৰক্ষাৰ বাবে পিন দিয়াটো বাধ্যতামূলক"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিৰিক্ত সুৰক্ষাৰ বাবে পাছৱর্ড দিয়াটো বাধ্যতামূলক"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্ৰশাসকে ডিভাইচ লক কৰি ৰাখিছে"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইচটো মেনুৱেলভাৱে লক কৰা হৈছিল"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"চিনাক্ত কৰিব পৰা নাই"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ডিফ’ল্ট"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"এনাল’গ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-az/strings.xml b/packages/SystemUI/res-keyguard/values-az/strings.xml
index 6ec1061..e125697 100644
--- a/packages/SystemUI/res-keyguard/values-az/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-az/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yenidən başladıqdan sonra model tələb olunur"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıqdan sonra PIN tələb olunur"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıqdan sonra parol tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Əlavə təhlükəsizlik üçün model tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Əlavə təhlükəsizlik üçün PIN tələb olunur"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Əlavə təhlükəsizlik üçün parol tələb olunur"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz admin tərəfindən kilidlənib"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihaz əl ilə kilidləndi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmır"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Defolt"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Qabarcıq"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoq"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
index 13d6613..f0d1ef2 100644
--- a/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-b+sr+Latn/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Treba da unesete šablon kada se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Treba da unesete PIN kada se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Treba da unesete lozinku kada se uređaj ponovo pokrene"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Treba da unesete šablon radi dodatne bezbednosti"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Treba da unesete PIN radi dodatne bezbednosti"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Treba da unesete lozinku radi dodatne bezbednosti"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznat"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Podrazumevani"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurići"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-be/strings.xml b/packages/SystemUI/res-keyguard/values-be/strings.xml
index 616d31a..e1af3ece 100644
--- a/packages/SystemUI/res-keyguard/values-be/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-be/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Пасля перазапуску прылады патрабуецца ўзор"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Пасля перазапуску прылады патрабуецца PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Пасля перазапуску прылады патрабуецца пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для забеспячэння дадатковай бяспекі патрабуецца ўзор"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для забеспячэння дадатковай бяспекі патрабуецца PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для забеспячэння дадатковай бяспекі патрабуецца пароль"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Прылада заблакіравана адміністратарам"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Прылада была заблакіравана ўручную"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распазнана"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Стандартны"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бурбалкі"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Са стрэлкамі"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bg/strings.xml b/packages/SystemUI/res-keyguard/values-bg/strings.xml
index 366a7f4..0b4417a 100644
--- a/packages/SystemUI/res-keyguard/values-bg/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bg/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"След рестартиране на устройството се изисква фигура"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"След рестартиране на устройството се изисква ПИН код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"След рестартиране на устройството се изисква парола"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"За допълнителна сигурност се изисква фигура"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"За допълнителна сигурност се изисква ПИН код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"За допълнителна сигурност се изисква парола"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройството е заключено от администратора"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройството бе заключено ръчно"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не е разпознато"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Стандартен"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Балонен"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогов"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bn/strings.xml b/packages/SystemUI/res-keyguard/values-bn/strings.xml
index c20be5d..4851579 100644
--- a/packages/SystemUI/res-keyguard/values-bn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bn/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ডিভাইসটি পুনরায় চালু হওয়ার পর প্যাটার্নের প্রয়োজন হবে"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ডিভাইসটি পুনরায় চালু হওয়ার পর পিন প্রয়োজন হবে"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ডিভাইসটি পুনরায় চালু হওয়ার পর পাসওয়ার্ডের প্রয়োজন হবে"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"অতিরিক্ত সুরক্ষার জন্য প্যাটার্ন দেওয়া প্রয়োজন"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"অতিরিক্ত সুরক্ষার জন্য পিন দেওয়া প্রয়োজন"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"অতিরিক্ত সুরক্ষার জন্য পাসওয়ার্ড দেওয়া প্রয়োজন"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"প্রশাসক ডিভাইসটি লক করেছেন"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ডিভাইসটিকে ম্যানুয়ালি লক করা হয়েছে"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"শনাক্ত করা যায়নি"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ডিফল্ট"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"বাবল"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"অ্যানালগ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-bs/strings.xml b/packages/SystemUI/res-keyguard/values-bs/strings.xml
index f1c00a9..4705b4d9 100644
--- a/packages/SystemUI/res-keyguard/values-bs/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-bs/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Potreban je uzorak nakon što se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Potreban je PIN nakon što se uređaj ponovo pokrene"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Potrebna je lozinka nakon što se uređaj ponovo pokrene"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Uzorak je potreban radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN je potreban radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lozinka je potrebna radi dodatne sigurnosti"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Uređaj je zaključao administrator"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurići"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ca/strings.xml b/packages/SystemUI/res-keyguard/values-ca/strings.xml
index 709407c..284eaeb 100644
--- a/packages/SystemUI/res-keyguard/values-ca/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ca/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cal introduir el patró quan es reinicia el dispositiu"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cal introduir el PIN quan es reinicia el dispositiu"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cal introduir la contrasenya quan es reinicia el dispositiu"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Cal introduir el patró per disposar de més seguretat"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Cal introduir el PIN per disposar de més seguretat"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Cal introduir la contrasenya per disposar de més seguretat"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'administrador ha bloquejat el dispositiu"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositiu s\'ha bloquejat manualment"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No s\'ha reconegut"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminada"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bombolla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analògica"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-cs/strings.xml b/packages/SystemUI/res-keyguard/values-cs/strings.xml
index a44658c..6b4f607 100644
--- a/packages/SystemUI/res-keyguard/values-cs/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-cs/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po restartování zařízení je vyžadováno gesto"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po restartování zařízení je vyžadován kód PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po restartování zařízení je vyžadováno heslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pro ještě lepší zabezpečení je vyžadováno gesto"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pro ještě lepší zabezpečení je vyžadován kód PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pro ještě lepší zabezpečení je vyžadováno heslo"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zařízení je uzamknuto administrátorem"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zařízení bylo ručně uzamčeno"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznáno"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Výchozí"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogové"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-da/strings.xml b/packages/SystemUI/res-keyguard/values-da/strings.xml
index 331c355..85238df 100644
--- a/packages/SystemUI/res-keyguard/values-da/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-da/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du skal angive et mønster, når du har genstartet enheden"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Der skal angives en pinkode efter genstart af enheden"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Der skal angives en adgangskode efter genstart af enheden"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Der kræves et mønster som ekstra beskyttelse"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Der kræves en pinkode som ekstra beskyttelse"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Der kræves en adgangskode som ekstra beskyttelse"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheden er blevet låst af administratoren"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheden blev låst manuelt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke genkendt"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-de/strings.xml b/packages/SystemUI/res-keyguard/values-de/strings.xml
index c19b357..18befed 100644
--- a/packages/SystemUI/res-keyguard/values-de/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-de/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nach dem Neustart des Geräts ist die Eingabe des Musters erforderlich"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nach dem Neustart des Geräts ist die Eingabe der PIN erforderlich"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nach dem Neustart des Geräts ist die Eingabe des Passworts erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zur Verbesserung der Sicherheit ist ein Muster erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zur Verbesserung der Sicherheit ist eine PIN erforderlich"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zur Verbesserung der Sicherheit ist ein Passwort erforderlich"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Gerät vom Administrator gesperrt"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Gerät manuell gesperrt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nicht erkannt"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-el/strings.xml b/packages/SystemUI/res-keyguard/values-el/strings.xml
index 1d6ec82..65b84486 100644
--- a/packages/SystemUI/res-keyguard/values-el/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-el/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Απαιτείται μοτίβο μετά από την επανεκκίνηση της συσκευής"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Απαιτείται PIN μετά από την επανεκκίνηση της συσκευής"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Απαιτείται κωδικός πρόσβασης μετά από την επανεκκίνηση της συσκευής"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Απαιτείται μοτίβο για πρόσθετη ασφάλεια"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Απαιτείται PIN για πρόσθετη ασφάλεια"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Απαιτείται κωδικός πρόσβασης για πρόσθετη ασφάλεια"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Η συσκευή κλειδώθηκε από τον διαχειριστή"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Η συσκευή κλειδώθηκε με μη αυτόματο τρόπο"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Δεν αναγνωρίστηκε"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Προεπιλογή"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Συννεφάκι"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Αναλογικό"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
index 2b78f96..588f1b5 100644
--- a/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rAU/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
index e1c2532..08fc8d6 100644
--- a/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rCA/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
index 2b78f96..588f1b5 100644
--- a/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rGB/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
index 2b78f96..588f1b5 100644
--- a/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rIN/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pattern required after device restarts"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN required after device restarts"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password required after device restarts"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pattern required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN required for additional security"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password required for additional security"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Device locked by admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Device was locked manually"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Not recognised"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogue"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
index 9052e4f..a23aeb0 100644
--- a/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-en-rXC/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎Pattern required after device restarts‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‎‎‎‎‎‏‎‎‎‏‎‎‎‏‎‏‎‏‏‏‎‎‎‎‎‏‎‏‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‎‏‏‎‏‏‎‏‏‏‎‎‎‎PIN required after device restarts‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‎‏‏‏‏‎‏‎‎‎‏‏‏‎‎‏‎‎‏‎‎‎‎‎‏‏‏‎‎‎‏‏‎‎‎‎‏‎‎Password required after device restarts‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‎‏‎‏‏‏‎‎‏‎‏‏‎‎‏‎‎‎‏‎Pattern required for additional security‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‎‎‎‎‏‏‎‎‎‏‎‏‏‎‏‎‎‎‎‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‎‎‏‎‎‎‎‏‎‎‎‎‎‏‎‎‎‏‎PIN required for additional security‎‏‎‎‏‎"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‎‏‎‏‏‎‎Password required for additional security‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‎‏‎‎‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‎For additional security, use pattern instead‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‏‏‎‎‏‎‏‏‏‏‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‏‏‎‎‎‎‎For additional security, use PIN instead‎‏‎‎‏‎"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‎‏‏‎‎‏‏‎‎‎‏‏‎‏‎‎‎‎‏‏‏‏‏‎‏‎‎For additional security, use password instead‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‎‎‏‏‎‏‏‎‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‎‏‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎‎‎‎‏‎‏‎Device locked by admin‎‏‎‎‏‎"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‏‏‎‎‎‎‎‏‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎Device was locked manually‎‏‎‎‏‎"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‎‎‎‎‎‏‏‏‏‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‏‏‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎Not recognized‎‏‎‎‏‎"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‏‎‎‎‎Default‎‏‎‎‏‎"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‎‏‎‎‏‏‎‎‎‎‎‏‎‏‎‏‏‎‎‎‏‎‏‏‏‎‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‏‎‏‎‏‏‏‏‎‏‎Bubble‎‏‎‎‏‎"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‎‎‎‎‎‎‏‎Analog‎‏‎‎‏‎"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‎‏‏‏‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‏‏‏‎‎‎‎‏‎‎‏‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‏‏‎‏‏‏‏‎Unlock your device to continue‎‏‎‎‏‎"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
index 9dc054a..c71a678 100644
--- a/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-es-rUS/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Se requiere el patrón después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Se requiere el PIN después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Se requiere la contraseña después de reiniciar el dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Se requiere el patrón por razones de seguridad"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Se requiere el PIN por razones de seguridad"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Se requiere la contraseña por razones de seguridad"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se bloqueó de forma manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoció"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-es/strings.xml b/packages/SystemUI/res-keyguard/values-es/strings.xml
index f9f0452..c6ee698 100644
--- a/packages/SystemUI/res-keyguard/values-es/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-es/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Debes introducir el patrón después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Debes introducir el PIN después de reiniciar el dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Debes introducir la contraseña después de reiniciar el dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Debes introducir el patrón como medida de seguridad adicional"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Debes introducir el PIN como medida de seguridad adicional"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Debes introducir la contraseña como medida de seguridad adicional"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado por el administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"El dispositivo se ha bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"No se reconoce"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuja"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-et/strings.xml b/packages/SystemUI/res-keyguard/values-et/strings.xml
index dceb78e..071ede8 100644
--- a/packages/SystemUI/res-keyguard/values-et/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-et/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pärast seadme taaskäivitamist tuleb sisestada muster"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pärast seadme taaskäivitamist tuleb sisestada PIN-kood"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pärast seadme taaskäivitamist tuleb sisestada parool"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Lisaturvalisuse huvides tuleb sisestada muster"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Lisaturvalisuse huvides tuleb sisestada PIN-kood"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Lisaturvalisuse huvides tuleb sisestada parool"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administraator lukustas seadme"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Seade lukustati käsitsi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tuvastatud"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Vaikenumbrilaud"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mull"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-eu/strings.xml b/packages/SystemUI/res-keyguard/values-eu/strings.xml
index 8431268..9b8e65b 100644
--- a/packages/SystemUI/res-keyguard/values-eu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-eu/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Eredua marraztu beharko duzu gailua berrabiarazten denean"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PINa idatzi beharko duzu gailua berrabiarazten denean"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pasahitza idatzi beharko duzu gailua berrabiarazten denean"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Eredua behar da gailua babestuago izateko"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PINa behar da gailua babestuago izateko"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Pasahitza behar da gailua babestuago izateko"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratzaileak blokeatu egin du gailua"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Eskuz blokeatu da gailua"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ez da ezagutu"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Lehenetsia"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Puxikak"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogikoa"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fa/strings.xml b/packages/SystemUI/res-keyguard/values-fa/strings.xml
index 37bb260..3583f1e 100644
--- a/packages/SystemUI/res-keyguard/values-fa/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fa/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"بعد از بازنشانی دستگاه باید الگو وارد شود"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"بعد از بازنشانی دستگاه باید پین وارد شود"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"بعد از بازنشانی دستگاه باید گذرواژه وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"برای ایمنی بیشتر باید الگو وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"برای ایمنی بیشتر باید پین وارد شود"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"برای ایمنی بیشتر باید گذرواژه وارد شود"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"دستگاه توسط سرپرست سیستم قفل شده است"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"دستگاه به‌صورت دستی قفل شده است"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"شناسایی نشد"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"پیش‌فرض"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"حباب"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"آنالوگ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fi/strings.xml b/packages/SystemUI/res-keyguard/values-fi/strings.xml
index f8cec42..a0ac6df 100644
--- a/packages/SystemUI/res-keyguard/values-fi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fi/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kuvio vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-koodi vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Salasana vaaditaan laitteen uudelleenkäynnistyksen jälkeen."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kuvio vaaditaan suojauksen parantamiseksi."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-koodi vaaditaan suojauksen parantamiseksi."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Salasana vaaditaan suojauksen parantamiseksi."</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Järjestelmänvalvoja lukitsi laitteen."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Laite lukittiin manuaalisesti"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ei tunnistettu"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Oletus"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Kupla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoginen"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
index 077fe11..66fd7c0 100644
--- a/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fr-rCA/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Le schéma est exigé après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Le NIP est exigé après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Le mot de passe est exigé après le redémarrage de l\'appareil"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Le schéma est exigé pour plus de sécurité"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Le NIP est exigé pour plus de sécurité"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Le mot de passe est exigé pour plus de sécurité"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Pour plus de sécurité, utilisez plutôt un schéma"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Pour plus de sécurité, utilisez plutôt un NIP"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Pour plus de sécurité, utilisez plutôt un mot de passe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"L\'appareil a été verrouillé par l\'administrateur"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"L\'appareil a été verrouillé manuellement"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Doigt non reconnu"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Déverrouiller votre appareil pour continuer"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-fr/strings.xml b/packages/SystemUI/res-keyguard/values-fr/strings.xml
index 45dadc1..ec00ba3 100644
--- a/packages/SystemUI/res-keyguard/values-fr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-fr/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Veuillez dessiner le schéma après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Veuillez saisir le code après le redémarrage de l\'appareil"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Veuillez saisir le mot de passe après le redémarrage de l\'appareil"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Veuillez dessiner le schéma pour renforcer la sécurité"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Veuillez saisir le code pour renforcer la sécurité"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Veuillez saisir le mot de passe pour renforcer la sécurité"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Appareil verrouillé par l\'administrateur"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Appareil verrouillé manuellement"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non reconnu"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Par défaut"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bulle"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogique"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-gl/strings.xml b/packages/SystemUI/res-keyguard/values-gl/strings.xml
index 4fbdd67..a3f8e86 100644
--- a/packages/SystemUI/res-keyguard/values-gl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-gl/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necesario o padrón despois do reinicio do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necesario o PIN despois do reinicio do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necesario o contrasinal despois do reinicio do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"É necesario o padrón para obter seguranza adicional"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"É necesario o PIN para obter seguranza adicional"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"É necesario o contrasinal para obter seguranza adicional"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"O administrador bloqueou o dispositivo"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo bloqueouse manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non se recoñeceu"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predeterminado"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbulla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analóxico"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-gu/strings.xml b/packages/SystemUI/res-keyguard/values-gu/strings.xml
index 6caac8a..c97fe01 100644
--- a/packages/SystemUI/res-keyguard/values-gu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-gu/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પૅટર્ન જરૂરી છે"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પિન જરૂરી છે"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ઉપકરણનો પુનઃપ્રારંભ થાય તે પછી પાસવર્ડ જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"વધારાની સુરક્ષા માટે પૅટર્ન જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"વધારાની સુરક્ષા માટે પિન જરૂરી છે"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"વધારાની સુરક્ષા માટે પાસવર્ડ જરૂરી છે"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"વ્યવસ્થાપકે ઉપકરણ લૉક કરેલું છે"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ઉપકરણ મેન્યુઅલી લૉક કર્યું હતું"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ઓળખાયેલ નથી"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ડિફૉલ્ટ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"બબલ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"એનાલોગ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hi/strings.xml b/packages/SystemUI/res-keyguard/values-hi/strings.xml
index 627576e..1283004 100644
--- a/packages/SystemUI/res-keyguard/values-hi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hi/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिवाइस फिर से चालू होने के बाद पैटर्न ज़रूरी है"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिवाइस फिर से चालू होने के बाद पिन ज़रूरी है"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिवाइस फिर से चालू होने के बाद पासवर्ड ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षा के लिए पैटर्न ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षा के लिए पिन ज़रूरी है"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षा के लिए पासवर्ड ज़रूरी है"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"व्यवस्थापक ने डिवाइस को लॉक किया है"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिवाइस को मैन्युअल रूप से लॉक किया गया था"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहचान नहीं हो पाई"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"डिफ़ॉल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"एनालॉग"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hr/strings.xml b/packages/SystemUI/res-keyguard/values-hr/strings.xml
index 8b1b504..7a14a80 100644
--- a/packages/SystemUI/res-keyguard/values-hr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hr/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Nakon ponovnog pokretanja uređaja morate unijeti uzorak"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Nakon ponovnog pokretanja uređaja morate unijeti PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Nakon ponovnog pokretanja uređaja morate unijeti zaporku"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Unesite uzorak radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Unesite PIN radi dodatne sigurnosti"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Unesite zaporku radi dodatne sigurnosti"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrator je zaključao uređaj"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Uređaj je ručno zaključan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nije prepoznato"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Zadano"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mjehurić"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogni"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hu/strings.xml b/packages/SystemUI/res-keyguard/values-hu/strings.xml
index 6b75e72..a4fbf53 100644
--- a/packages/SystemUI/res-keyguard/values-hu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hu/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Az eszköz újraindítását követően meg kell adni a mintát"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Az eszköz újraindítását követően meg kell adni a PIN-kódot"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Az eszköz újraindítását követően meg kell adni a jelszót"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"A nagyobb biztonság érdekében minta szükséges"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"A nagyobb biztonság érdekében PIN-kód szükséges"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A nagyobb biztonság érdekében jelszó szükséges"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"A rendszergazda zárolta az eszközt"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Az eszközt manuálisan lezárták"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nem ismerhető fel"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Alapértelmezett"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Buborék"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analóg"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-hy/strings.xml b/packages/SystemUI/res-keyguard/values-hy/strings.xml
index 3412026..086eeb9 100644
--- a/packages/SystemUI/res-keyguard/values-hy/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-hy/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել նախշը"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել PIN կոդը"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Սարքը վերագործարկելուց հետո անհրաժեշտ է մուտքագրել գաղտնաբառը"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել նախշը"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել PIN կոդը"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Անվտանգության նկատառումներից ելնելով անհրաժեշտ է մուտքագրել գաղտնաբառը"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Սարքը կողպված է ադմինիստրատորի կողմից"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Սարքը կողպվել է ձեռքով"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Չհաջողվեց ճանաչել"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Կանխադրված"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Պղպջակ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Անալոգային"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-in/strings.xml b/packages/SystemUI/res-keyguard/values-in/strings.xml
index 1afb791..b43a032 100644
--- a/packages/SystemUI/res-keyguard/values-in/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-in/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pola diperlukan setelah perangkat dimulai ulang"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah perangkat dimulai ulang"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Sandi diperlukan setelah perangkat dimulai ulang"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Pola diperlukan untuk keamanan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keamanan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Sandi diperlukan untuk keamanan tambahan"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Perangkat dikunci oleh admin"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Perangkat dikunci secara manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-is/strings.xml b/packages/SystemUI/res-keyguard/values-is/strings.xml
index 6abdc82..8bad961 100644
--- a/packages/SystemUI/res-keyguard/values-is/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-is/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Mynsturs er krafist þegar tækið er endurræst"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN-númers er krafist þegar tækið er endurræst"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Aðgangsorðs er krafist þegar tækið er endurræst"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mynsturs er krafist af öryggisástæðum"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN-númers er krafist af öryggisástæðum"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Aðgangsorðs er krafist af öryggisástæðum"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Kerfisstjóri læsti tæki"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Tækinu var læst handvirkt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Þekktist ekki"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Sjálfgefið"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Blaðra"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Með vísum"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-it/strings.xml b/packages/SystemUI/res-keyguard/values-it/strings.xml
index 9fed5f7..186177ff 100644
--- a/packages/SystemUI/res-keyguard/values-it/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-it/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Sequenza obbligatoria dopo il riavvio del dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN obbligatorio dopo il riavvio del dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Password obbligatoria dopo il riavvio del dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Sequenza obbligatoria per maggiore sicurezza"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN obbligatorio per maggiore sicurezza"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Password obbligatoria per maggiore sicurezza"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloccato dall\'amministratore"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Il dispositivo è stato bloccato manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Non riconosciuto"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predefinito"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogico"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-iw/strings.xml b/packages/SystemUI/res-keyguard/values-iw/strings.xml
index b5b1c53..aab4206 100644
--- a/packages/SystemUI/res-keyguard/values-iw/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-iw/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"יש להזין את קו ביטול הנעילה לאחר הפעלה מחדש של המכשיר"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"צריך להזין קוד אימות לאחר הפעלה מחדש של המכשיר"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"יש להזין סיסמה לאחר הפעלה מחדש של המכשיר"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"יש להזין את קו ביטול הנעילה כדי להגביר את רמת האבטחה"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"יש להזין קוד אימות כדי להגביר את רמת האבטחה"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"יש להזין סיסמה כדי להגביר את רמת האבטחה"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקו ביטול נעילה במקום זאת"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"כדי להגביר את רמת האבטחה, כדאי להשתמש בקוד אימות במקום זאת"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"כדי להגביר את רמת האבטחה, כדאי להשתמש בסיסמה במקום זאת"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"המנהל של המכשיר נהל אותו"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"המכשיר ננעל באופן ידני"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"לא זוהתה"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ברירת מחדל"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"בועה"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"אנלוגי"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"צריך לבטל את הנעילה של המכשיר כדי להמשיך"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ja/strings.xml b/packages/SystemUI/res-keyguard/values-ja/strings.xml
index afe0159..1a4fb0b 100644
--- a/packages/SystemUI/res-keyguard/values-ja/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ja/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"デバイスの再起動後はパターンの入力が必要となります"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"デバイスの再起動後は PIN の入力が必要となります"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"デバイスの再起動後はパスワードの入力が必要となります"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"追加の確認のためパターンが必要です"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"追加の確認のため PIN が必要です"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"追加の確認のためパスワードが必要です"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"セキュリティを強化するには代わりにパターンを使用してください"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"セキュリティを強化するには代わりに PIN を使用してください"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"セキュリティを強化するには代わりにパスワードを使用してください"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"デバイスは管理者によりロックされています"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"デバイスは手動でロックされました"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"認識されませんでした"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"デフォルト"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"バブル"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"アナログ"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"続行するにはデバイスのロックを解除してください"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ka/strings.xml b/packages/SystemUI/res-keyguard/values-ka/strings.xml
index b32caa7..123cc39 100644
--- a/packages/SystemUI/res-keyguard/values-ka/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ka/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა ნიმუშის დახატვა"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა PIN-კოდის შეყვანა"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"მოწყობილობის გადატვირთვის შემდეგ საჭიროა პაროლის შეყვანა"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"დამატებითი უსაფრთხოებისთვის საჭიროა ნიმუშის დახატვა"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"დამატებითი უსაფრთხოებისთვის საჭიროა PIN-კოდის შეყვანა"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"დამატებითი უსაფრთხოებისთვის საჭიროა პაროლის შეყვანა"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"მოწყობილობა ჩაკეტილია ადმინისტრატორის მიერ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"მოწყობილობა ხელით ჩაიკეტა"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"არ არის ამოცნობილი"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ნაგულისხმევი"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ბუშტი"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ანალოგური"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-kk/strings.xml b/packages/SystemUI/res-keyguard/values-kk/strings.xml
index d6d5bcd..8daca5c 100644
--- a/packages/SystemUI/res-keyguard/values-kk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-kk/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Құрылғы қайта іске қосылғаннан кейін, өрнекті енгізу қажет"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Құрылғы қайта іске қосылғаннан кейін, PIN кодын енгізу қажет"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Құрылғы қайта іске қосылғаннан кейін, құпия сөзді енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Қауіпсіздікті күшейту үшін өрнекті енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Қауіпсіздікті күшейту үшін PIN кодын енгізу қажет"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Қауіпсіздікті күшейту үшін құпия сөзді енгізу қажет"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Құрылғыны әкімші құлыптаған"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Құрылғы қолмен құлыпталды"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Танылмады"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Әдепкі"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Көпіршік"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогтық"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-km/strings.xml b/packages/SystemUI/res-keyguard/values-km/strings.xml
index 00bfe05..73f507c 100644
--- a/packages/SystemUI/res-keyguard/values-km/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-km/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"តម្រូវឲ្យប្រើលំនាំ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"តម្រូវឲ្យបញ្ចូលកូដ PIN បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ បន្ទាប់ពីឧបករណ៍ចាប់ផ្តើមឡើងវិញ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"តម្រូវឲ្យប្រើលំនាំ ដើម្បីទទួលបានសវុត្ថិភាពបន្ថែម"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"តម្រូវឲ្យបញ្ចូលកូដ PIN ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"តម្រូវឲ្យបញ្ចូលពាក្យសម្ងាត់ ដើម្បីទទួលបានសុវត្ថិភាពបន្ថែម"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ឧបករណ៍​ត្រូវបាន​ចាក់សោ​ដោយអ្នក​គ្រប់គ្រង"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ឧបករណ៍ត្រូវបានចាក់សោដោយអ្នកប្រើផ្ទាល់"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"មិនអាចសម្គាល់បានទេ"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"លំនាំដើម"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ពពុះ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"អាណាឡូក"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-kn/strings.xml b/packages/SystemUI/res-keyguard/values-kn/strings.xml
index 80a98e6..c279cea 100644
--- a/packages/SystemUI/res-keyguard/values-kn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-kn/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಿನ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ಸಾಧನ ಮರುಪ್ರಾರಂಭಗೊಂಡ ನಂತರ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿರುತ್ತದೆ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪ್ಯಾಟರ್ನ್ ಅಗತ್ಯವಿದೆ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗೆ ಪಿನ್ ಅಗತ್ಯವಿದೆ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ಹೆಚ್ಚುವರಿ ಭದ್ರತೆಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿದೆ"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ನಿರ್ವಾಹಕರು ಸಾಧನವನ್ನು ಲಾಕ್ ಮಾಡಿದ್ದಾರೆ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ಸಾಧನವನ್ನು ಹಸ್ತಚಾಲಿತವಾಗಿ ಲಾಕ್‌ ಮಾಡಲಾಗಿದೆ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ಗುರುತಿಸಲಾಗಿಲ್ಲ"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ಡೀಫಾಲ್ಟ್"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ಬಬಲ್"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ಅನಲಾಗ್"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ko/strings.xml b/packages/SystemUI/res-keyguard/values-ko/strings.xml
index b952f1b..4c058ed 100644
--- a/packages/SystemUI/res-keyguard/values-ko/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ko/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"기기가 다시 시작되면 패턴이 필요합니다."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"기기가 다시 시작되면 PIN이 필요합니다."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"기기가 다시 시작되면 비밀번호가 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"보안 강화를 위해 패턴이 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"보안 강화를 위해 PIN이 필요합니다."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"보안 강화를 위해 비밀번호가 필요합니다."</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"관리자가 기기를 잠갔습니다."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"기기가 수동으로 잠금 설정되었습니다."</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"인식할 수 없음"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"기본"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"버블"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"아날로그"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ky/strings.xml b/packages/SystemUI/res-keyguard/values-ky/strings.xml
index 485337d..7c7099e 100644
--- a/packages/SystemUI/res-keyguard/values-ky/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ky/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Түзмөк кайра күйгүзүлгөндөн кийин графикалык ачкычты тартуу талап кылынат"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Түзмөк кайра күйгүзүлгөндөн кийин PIN-кодду киргизүү талап кылынат"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Түзмөк кайра күйгүзүлгөндөн кийин сырсөздү киргизүү талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Коопсуздукту бекемдөө үчүн графикалык ачкыч талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Коопсуздукту бекемдөө үчүн PIN-код талап кылынат"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Коопсуздукту бекемдөө үчүн сырсөз талап кылынат"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Түзмөктү администратор кулпулап койгон"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Түзмөк кол менен кулпуланды"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таанылган жок"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Демейки"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Көбүк"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналог"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lo/strings.xml b/packages/SystemUI/res-keyguard/values-lo/strings.xml
index 17584b5..5a6df42 100644
--- a/packages/SystemUI/res-keyguard/values-lo/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lo/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ຈຳເປັນຕ້ອງມີແບບຮູບປົດລັອກຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ຈຳເປັນຕ້ອງມີ PIN ຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານຫຼັງຈາກອຸປະກອນເລີ່ມລະບົບໃໝ່"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ຈຳເປັນຕ້ອງມີແບບຮູບເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ຈຳເປັນຕ້ອງມີ PIN ເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ຈຳເປັນຕ້ອງມີລະຫັດຜ່ານເພື່ອຄວາມປອດໄພເພີ່ມເຕີມ"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ອຸປະກອນຖືກລັອກໂດຍຜູ້ເບິ່ງແຍງລະບົບ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ອຸປະກອນຖືກສັ່ງໃຫ້ລັອກ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ບໍ່ຮູ້ຈັກ"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ຄ່າເລີ່ມຕົ້ນ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ຟອງ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ໂມງເຂັມ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lt/strings.xml b/packages/SystemUI/res-keyguard/values-lt/strings.xml
index a066a66..4d98fd1 100644
--- a/packages/SystemUI/res-keyguard/values-lt/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lt/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iš naujo paleidus įrenginį būtinas atrakinimo piešinys"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iš naujo paleidus įrenginį būtinas PIN kodas"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iš naujo paleidus įrenginį būtinas slaptažodis"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Norint užtikrinti papildomą saugą būtinas atrakinimo piešinys"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Norint užtikrinti papildomą saugą būtinas PIN kodas"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Norint užtikrinti papildomą saugą būtinas slaptažodis"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Įrenginį užrakino administratorius"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Įrenginys užrakintas neautomatiškai"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Neatpažinta"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Numatytasis"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Debesėlis"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoginis"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-lv/strings.xml b/packages/SystemUI/res-keyguard/values-lv/strings.xml
index d371a4b..2660a06 100644
--- a/packages/SystemUI/res-keyguard/values-lv/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-lv/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Pēc ierīces restartēšanas ir jāievada atbloķēšanas kombinācija."</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pēc ierīces restartēšanas ir jāievada PIN kods."</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Pēc ierīces restartēšanas ir jāievada parole."</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Papildu drošībai ir jāievada atbloķēšanas kombinācija."</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Papildu drošībai ir jāievada PIN kods."</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Papildu drošībai ir jāievada parole."</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administrators bloķēja ierīci."</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Ierīce tika bloķēta manuāli."</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nav atpazīts"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Noklusējums"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Burbuļi"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogais"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mk/strings.xml b/packages/SystemUI/res-keyguard/values-mk/strings.xml
index ef22564..77e1b50 100644
--- a/packages/SystemUI/res-keyguard/values-mk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mk/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Потребна е шема по рестартирање на уредот"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Потребен е PIN-код по рестартирање на уредот"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Потребна е лозинка по рестартирање на уредот"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Потребна е шема за дополнителна безбедност"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Потребен е PIN-код за дополнителна безбедност"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Потребна е лозинка за дополнителна безбедност"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"За дополнителна безбедност, користете шема"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"За дополнителна безбедност, користете PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"За дополнителна безбедност, користете лозинка"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Уредот е заклучен од администраторот"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уредот е заклучен рачно"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Непознат"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Стандарден"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Балонче"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналоген"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Отклучете го уредот за да продолжите"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ml/strings.xml b/packages/SystemUI/res-keyguard/values-ml/strings.xml
index 63a542a..e62b435 100644
--- a/packages/SystemUI/res-keyguard/values-ml/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ml/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പാറ്റേൺ വരയ്‌ക്കേണ്ടതുണ്ട്"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പിൻ നൽകേണ്ടതുണ്ട്"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ഉപകരണം റീസ്റ്റാർട്ടായശേഷം ‌പാസ്‌വേഡ് നൽകേണ്ടതുണ്ട്"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"അധിക സുരക്ഷയ്ക്ക് പാറ്റേൺ ആവശ്യമാണ്"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"അധിക സുരക്ഷയ്ക്ക് പിൻ ആവശ്യമാണ്"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"അധിക സുരക്ഷയ്ക്ക് പാസ്‌വേഡ് ആവശ്യമാണ്"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ഉപകരണം അഡ്‌മിൻ ലോക്കുചെയ്തു"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ഉപകരണം നേരിട്ട് ലോക്കുചെയ്തു"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"തിരിച്ചറിയുന്നില്ല"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ഡിഫോൾട്ട്"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ബബിൾ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"അനലോഗ്"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mn/strings.xml b/packages/SystemUI/res-keyguard/values-mn/strings.xml
index 71c913f..f2cc5ab 100644
--- a/packages/SystemUI/res-keyguard/values-mn/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mn/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Төхөөрөмжийг дахин эхлүүлсний дараа загвар оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Төхөөрөмжийг дахин эхлүүлсний дараа ПИН оруулах шаардлагатай"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Төхөөрөмжийг дахин эхлүүлсний дараа нууц үг оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Аюулгүй байдлын үүднээс загвар оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Аюулгүй байдлын үүднээс ПИН оруулах шаардлагатай"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Аюулгүй байдлын үүднээс нууц үг оруулах шаардлагатай"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Нэмэлт аюулгүй байдлын үүднээс оронд нь хээ ашиглана уу"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Нэмэлт аюулгүй байдлын үүднээс оронд нь ПИН ашиглана уу"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Нэмэлт аюулгүй байдлын үүднээс оронд нь нууц үг ашиглана уу"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Админ төхөөрөмжийг түгжсэн"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Төхөөрөмжийг гараар түгжсэн"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Таньж чадсангүй"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Өгөгдмөл"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бөмбөлөг"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Aналог"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Үргэлжлүүлэхийн тулд төхөөрөмжийнхөө түгжээг тайлна уу"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-mr/strings.xml b/packages/SystemUI/res-keyguard/values-mr/strings.xml
index 6ac13bd..1454b20 100644
--- a/packages/SystemUI/res-keyguard/values-mr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-mr/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"डिव्हाइस रीस्टार्ट झाल्यावर पॅटर्न आवश्यक आहे"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"डिव्हाइस रीस्टार्ट झाल्यावर पिन आवश्यक आहे"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"डिव्हाइस रीस्टार्ट झाल्यावर पासवर्ड आवश्यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षिततेसाठी पॅटर्न आवश्‍यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षिततेसाठी पिन आवश्‍यक आहे"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षिततेसाठी पासवर्ड आवश्‍यक आहे"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकाद्वारे लॉक केलेले डिव्हाइस"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"डिव्हाइस मॅन्युअली लॉक केले होते"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ओळखले नाही"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"डीफॉल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"अ‍ॅनालॉग"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ms/strings.xml b/packages/SystemUI/res-keyguard/values-ms/strings.xml
index 453afc3..a6d1af9 100644
--- a/packages/SystemUI/res-keyguard/values-ms/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ms/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Corak diperlukan setelah peranti dimulakan semula"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"PIN diperlukan setelah peranti dimulakan semula"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kata laluan diperlukan setelah peranti dimulakan semula"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Corak diperlukan untuk keselamatan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN diperlukan untuk keselamatan tambahan"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kata laluan diperlukan untuk keselamatan tambahan"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Peranti dikunci oleh pentadbir"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Peranti telah dikunci secara manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tidak dikenali"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Lalai"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Gelembung"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-my/strings.xml b/packages/SystemUI/res-keyguard/values-my/strings.xml
index 1cc46b1..5617a11 100644
--- a/packages/SystemUI/res-keyguard/values-my/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-my/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပုံစံ လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် ပင်နံပါတ် လိုအပ်ပါသည်"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"စက်ပစ္စည်းကို ပိတ်ပြီးပြန်ဖွင့်လိုက်သည့်အခါတွင် စကားဝှက် လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပုံစံ လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် ပင်နံပါတ် လိုအပ်ပါသည်"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ပိုမို၍ လုံခြုံမှု ရှိစေရန် စကားဝှက် လိုအပ်ပါသည်"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"စက်ပစ္စည်းကို စီမံခန့်ခွဲသူက လော့ခ်ချထားပါသည်"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"စက်ပစ္စည်းကို ကိုယ်တိုင်ကိုယ်ကျ လော့ခ်ချထားခဲ့သည်"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"မသိ"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"မူလ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ပူဖောင်းကွက်"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ရိုးရိုး"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-nb/strings.xml b/packages/SystemUI/res-keyguard/values-nb/strings.xml
index 5310a730..0ad9e95 100644
--- a/packages/SystemUI/res-keyguard/values-nb/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-nb/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du må tegne mønsteret etter at enheten har startet på nytt"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du må skrive inn PIN-koden etter at enheten har startet på nytt"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du må skrive inn passordet etter at enheten har startet på nytt"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du må tegne mønsteret for ekstra sikkerhet"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du må skrive inn PIN-koden for ekstra sikkerhet"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du må skrive inn passordet for ekstra sikkerhet"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Enheten er låst av administratoren"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten ble låst manuelt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ikke gjenkjent"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Boble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ne/strings.xml b/packages/SystemUI/res-keyguard/values-ne/strings.xml
index 534164b..196b74a 100644
--- a/packages/SystemUI/res-keyguard/values-ne/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ne/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"यन्त्र पुनः सुरु भएपछि ढाँचा आवश्यक पर्दछ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"यन्त्र पुनः सुरु भएपछि PIN आवश्यक पर्दछ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"यन्त्र पुनः सुरु भएपछि पासवर्ड आवश्यक पर्दछ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"अतिरिक्त सुरक्षाको लागि ढाँचा आवश्यक छ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"अतिरिक्त सुरक्षाको लागि PIN आवश्यक छ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"अतिरिक्त सुरक्षाको लागि पासवर्ड आवश्यक छ"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"प्रशासकले यन्त्रलाई लक गर्नुभएको छ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"यन्त्रलाई म्यानुअल तरिकाले लक गरिएको थियो"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"पहिचान भएन"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"डिफल्ट"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"बबल"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"एनालग"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-nl/strings.xml b/packages/SystemUI/res-keyguard/values-nl/strings.xml
index 08e226d4..747b3bb 100644
--- a/packages/SystemUI/res-keyguard/values-nl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-nl/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Patroon vereist nadat het apparaat opnieuw is opgestart"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Pincode vereist nadat het apparaat opnieuw is opgestart"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Wachtwoord vereist nadat het apparaat opnieuw is opgestart"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Patroon vereist voor extra beveiliging"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Pincode vereist voor extra beveiliging"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Wachtwoord vereist voor extra beveiliging"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Apparaat vergrendeld door beheerder"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Apparaat is handmatig vergrendeld"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Niet herkend"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standaard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bel"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-or/strings.xml b/packages/SystemUI/res-keyguard/values-or/strings.xml
index 3cdd264..75f7a89 100644
--- a/packages/SystemUI/res-keyguard/values-or/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-or/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାଟର୍ନ ଆବଶ୍ୟକ ଅଟେ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବାପରେ ପାସ୍‌ୱର୍ଡ ଆବଶ୍ୟକ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ଡିଭାଇସ୍‍ ରିଷ୍ଟାର୍ଟ ହେବା ପରେ ପାସୱର୍ଡ ଆବଶ୍ୟକ ଅଟେ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାଟର୍ନ ଆବଶ୍ୟକ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ PIN ଆବଶ୍ୟକ ଅଟେ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ ପାସ୍‌ୱର୍ଡ ଆବଶ୍ୟକ"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାଟର୍ନ ବ୍ୟବହାର କରନ୍ତୁ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ PIN ବ୍ୟବହାର କରନ୍ତୁ"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ଅତିରିକ୍ତ ସୁରକ୍ଷା ପାଇଁ, ଏହା ପରିବର୍ତ୍ତେ ପାସୱାର୍ଡ ବ୍ୟବହାର କରନ୍ତୁ"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ଡିଭାଇସ୍‍ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଲକ୍‍ କରାଯାଇଛି"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ଡିଭାଇସ୍‍ ମାନୁଆଲ ଭାବେ ଲକ୍‍ କରାଗଲା"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ଚିହ୍ନଟ ହେଲାନାହିଁ"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ଡିଫଲ୍ଟ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ବବଲ୍"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ଆନାଲଗ୍"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ଜାରି ରଖିବା ପାଇଁ ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅନଲକ କରନ୍ତୁ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pa/strings.xml b/packages/SystemUI/res-keyguard/values-pa/strings.xml
index 409f727..bf1a359a 100644
--- a/packages/SystemUI/res-keyguard/values-pa/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pa/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ਡੀਵਾਈਸ ਦੇ ਮੁੜ-ਚਾਲੂ ਹੋਣ \'ਤੇ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪੈਟਰਨ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਿੰਨ ਦੀ ਲੋੜ ਹੈ"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ਵਧੀਕ ਸੁਰੱਖਿਆ ਲਈ ਪਾਸਵਰਡ ਦੀ ਲੋੜ ਹੈ"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਡੀਵਾਈਸ ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"ਡੀਵਾਈਸ ਨੂੰ ਹੱਥੀਂ ਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"ਬੁਲਬੁਲਾ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ਐਨਾਲੌਗ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pl/strings.xml b/packages/SystemUI/res-keyguard/values-pl/strings.xml
index 52bc982..c49149b 100644
--- a/packages/SystemUI/res-keyguard/values-pl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pl/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po ponownym uruchomieniu urządzenia wymagany jest wzór"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po ponownym uruchomieniu urządzenia wymagany jest kod PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po ponownym uruchomieniu urządzenia wymagane jest hasło"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Dla większego bezpieczeństwa musisz narysować wzór"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Dla większego bezpieczeństwa musisz podać kod PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Dla większego bezpieczeństwa musisz podać hasło"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Urządzenie zablokowane przez administratora"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Urządzenie zostało zablokowane ręcznie"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nie rozpoznano"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Domyślna"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bąbelkowy"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogowy"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
index b934826..3d60e8c 100644
--- a/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt-rBR/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
index a67bfb0..0a94349 100644
--- a/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt-rPT/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"É necessário um padrão após reiniciar o dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"É necessário um PIN após reiniciar o dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"É necessária uma palavra-passe após reiniciar o dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Para segurança adicional, é necessário um padrão"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Para segurança adicional, é necessário um PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Para segurança adicional, é necessária uma palavra-passe"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para uma segurança adicional, use antes o padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para uma segurança adicional, use antes o PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para uma segurança adicional, use antes a palavra-passe"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo gestor"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido."</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predefinido"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balão"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-pt/strings.xml b/packages/SystemUI/res-keyguard/values-pt/strings.xml
index b934826..3d60e8c 100644
--- a/packages/SystemUI/res-keyguard/values-pt/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-pt/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"O padrão é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"O PIN é exigido após a reinicialização do dispositivo"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"A senha é exigida após a reinicialização do dispositivo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"O padrão é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"O PIN é necessário para aumentar a segurança"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"A senha é necessária para aumentar a segurança"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para ter mais segurança, use o padrão"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para ter mais segurança, use o PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para ter mais segurança, use a senha"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispositivo bloqueado pelo administrador"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"O dispositivo foi bloqueado manualmente"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Não reconhecido"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Padrão"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bolha"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógico"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Desbloqueie o dispositivo para continuar"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ro/strings.xml b/packages/SystemUI/res-keyguard/values-ro/strings.xml
index 5ee67d91..547224e 100644
--- a/packages/SystemUI/res-keyguard/values-ro/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ro/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Modelul este necesar după repornirea dispozitivului"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Codul PIN este necesar după repornirea dispozitivului"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Parola este necesară după repornirea dispozitivului"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Modelul este necesar pentru securitate suplimentară"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Codul PIN este necesar pentru securitate suplimentară"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Parola este necesară pentru securitate suplimentară"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Dispozitiv blocat de administrator"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Dispozitivul a fost blocat manual"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nu este recunoscut"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Prestabilit"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Balon"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogic"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ru/strings.xml b/packages/SystemUI/res-keyguard/values-ru/strings.xml
index 2b8f8d6..f1945ad 100644
--- a/packages/SystemUI/res-keyguard/values-ru/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ru/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"После перезагрузки устройства необходимо ввести графический ключ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"После перезагрузки устройства необходимо ввести PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"После перезагрузки устройства необходимо ввести пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"В качестве дополнительной меры безопасности введите графический ключ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"В качестве дополнительной меры безопасности введите PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"В качестве дополнительной меры безопасности введите пароль"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"В целях дополнительной безопасности используйте графический ключ"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"В целях дополнительной безопасности используйте PIN-код"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"В целях дополнительной безопасности используйте пароль"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Устройство заблокировано администратором"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Устройство было заблокировано вручную"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не распознано"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"По умолчанию"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Пузырь"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Стрелки"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Чтобы продолжить, разблокируйте устройство"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-si/strings.xml b/packages/SystemUI/res-keyguard/values-si/strings.xml
index 4e911de..e5862c3 100644
--- a/packages/SystemUI/res-keyguard/values-si/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-si/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"උපාංගය නැවත ආරම්භ වූ පසු රටාව අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"උපාංගය නැවත ආරම්භ වූ පසු PIN අංකය අවශ්‍යයි"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"උපාංගය නැවත ආරම්භ වූ පසු මුරපදය අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"අමතර ආරක්ෂාව සඳහා රටාව අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"අමතර ආරක්ෂාව සඳහා PIN අංකය අවශ්‍යයි"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"අමතර ආරක්ෂාව සඳහා මුරපදය අවශ්‍යයි"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ඔබගේ පරිපාලක විසින් උපාංගය අගුළු දමා ඇත"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"උපාංගය හස්තීයව අගුලු දමන ලදී"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"හඳුනා නොගන්නා ලදී"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"පෙරනිමි"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"බුබුළ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ප්‍රතිසමය"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sk/strings.xml b/packages/SystemUI/res-keyguard/values-sk/strings.xml
index f2d68e3..efe4ec8 100644
--- a/packages/SystemUI/res-keyguard/values-sk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sk/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po reštartovaní zariadenia musíte zadať bezpečnostný vzor"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po reštartovaní zariadenia musíte zadať kód PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po reštartovaní zariadenia musíte zadať heslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Na ďalšie zabezpečenie musíte zadať bezpečnostný vzor"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Na ďalšie zabezpečenie musíte zadať kód PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Na ďalšie zabezpečenie musíte zadať heslo"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Zariadenie zamkol správca"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Zariadenie bolo uzamknuté ručne"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nerozpoznané"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Predvolený"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bublina"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analógový"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sl/strings.xml b/packages/SystemUI/res-keyguard/values-sl/strings.xml
index 772308f..52726c2 100644
--- a/packages/SystemUI/res-keyguard/values-sl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sl/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Po vnovičnem zagonu naprave je treba vnesti vzorec"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Po vnovičnem zagonu naprave je treba vnesti kodo PIN"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Po vnovičnem zagonu naprave je treba vnesti geslo"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Zaradi dodatne varnosti morate vnesti vzorec"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Zaradi dodatne varnosti morate vnesti kodo PIN"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Zaradi dodatne varnosti morate vnesti geslo"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Napravo je zaklenil skrbnik"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Naprava je bila ročno zaklenjena"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Ni prepoznano"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Privzeto"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Mehurček"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogno"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sq/strings.xml b/packages/SystemUI/res-keyguard/values-sq/strings.xml
index c758462..a0a5594 100644
--- a/packages/SystemUI/res-keyguard/values-sq/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sq/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kërkohet motivi pas rinisjes së pajisjes"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kërkohet kodi PIN pas rinisjes së pajisjes"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kërkohet fjalëkalimi pas rinisjes së pajisjes"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kërkohet motivi për më shumë siguri"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kërkohet kodi PIN për më shumë siguri"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kërkohet fjalëkalimi për më shumë siguri"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Pajisja është e kyçur nga administratori"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Pajisja është kyçur manualisht"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Nuk njihet"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"E parazgjedhur"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Flluskë"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analoge"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sr/strings.xml b/packages/SystemUI/res-keyguard/values-sr/strings.xml
index e6fe853..e634fdcb5 100644
--- a/packages/SystemUI/res-keyguard/values-sr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sr/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Треба да унесете шаблон када се уређај поново покрене"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Треба да унесете PIN када се уређај поново покрене"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Треба да унесете лозинку када се уређај поново покрене"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Треба да унесете шаблон ради додатне безбедности"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Треба да унесете PIN ради додатне безбедности"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Треба да унесете лозинку ради додатне безбедности"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Администратор је закључао уређај"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Уређај је ручно закључан"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Није препознат"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Подразумевани"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Мехурићи"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналогни"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sv/strings.xml b/packages/SystemUI/res-keyguard/values-sv/strings.xml
index fa241d9..fc9beb1 100644
--- a/packages/SystemUI/res-keyguard/values-sv/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sv/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Du måste rita mönster när du har startat om enheten"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Du måste ange pinkod när du har startat om enheten"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Du måste ange lösenord när du har startat om enheten"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Du måste rita mönster för ytterligare säkerhet"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Du måste ange pinkod för ytterligare säkerhet"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Du måste ange lösenord för ytterligare säkerhet"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Administratören har låst enheten"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Enheten har låsts manuellt"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Identifierades inte"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Standard"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubbla"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-sw/strings.xml b/packages/SystemUI/res-keyguard/values-sw/strings.xml
index 791bceb..bcab24b 100644
--- a/packages/SystemUI/res-keyguard/values-sw/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-sw/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Unafaa kuchora mchoro baada ya kuwasha kifaa upya"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Unafaa kuweka PIN baada ya kuwasha kifaa upya"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Unafaa kuweka nenosiri baada ya kuwasha kifaa upya"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Mchoro unahitajika ili kuongeza usalama"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"PIN inahitajika ili kuongeza usalama"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Nenosiri linahitajika ili kuongeza usalama."</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Msimamizi amefunga kifaa"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Umefunga kifaa mwenyewe"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Haitambuliwi"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Chaguomsingi"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Kiputo"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analogi"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ta/strings.xml b/packages/SystemUI/res-keyguard/values-ta/strings.xml
index 271657d..88d5760 100644
--- a/packages/SystemUI/res-keyguard/values-ta/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ta/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"சாதனத்தை மீண்டும் தொடங்கியதும், பேட்டர்னை வரைய வேண்டும்"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"சாதனத்தை மீண்டும் தொடங்கியதும், பின்னை உள்ளிட வேண்டும்"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"சாதனத்தை மீண்டும் தொடங்கியதும், கடவுச்சொல்லை உள்ளிட வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"கூடுதல் பாதுகாப்பிற்கு, பேட்டர்னை வரைய வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"கூடுதல் பாதுகாப்பிற்கு, பின்னை உள்ளிட வேண்டும்"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"கூடுதல் பாதுகாப்பிற்கு, கடவுச்சொல்லை உள்ளிட வேண்டும்"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"நிர்வாகி சாதனத்தைப் பூட்டியுள்ளார்"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"பயனர் சாதனத்தைப் பூட்டியுள்ளார்"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"அடையாளங்காணபடவில்லை"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"இயல்பு"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"பபிள்"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"அனலாக்"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-te/strings.xml b/packages/SystemUI/res-keyguard/values-te/strings.xml
index f62e667..3a0111a 100644
--- a/packages/SystemUI/res-keyguard/values-te/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-te/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత నమూనాను గీయాలి"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"డివైజ్‌ను పునఃప్రారంభించిన తర్వాత పిన్ నమోదు చేయాలి"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"పరికరాన్ని పునఃప్రారంభించిన తర్వాత పాస్‌వర్డ్‌ను నమోదు చేయాలి"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"అదనపు సెక్యూరిటీ కోసం ఆకృతి అవసరం"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"అదనపు సెక్యూరిటీ కోసం పిన్ ఎంటర్ చేయాలి"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"అదనపు సెక్యూరిటీ కోసం పాస్‌వర్డ్‌ను ఎంటర్ చేయాలి"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"పరికరం నిర్వాహకుల ద్వారా లాక్ చేయబడింది"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"పరికరం మాన్యువల్‌గా లాక్ చేయబడింది"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"గుర్తించలేదు"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ఆటోమేటిక్"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"బబుల్"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"ఎనలాగ్"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-th/strings.xml b/packages/SystemUI/res-keyguard/values-th/strings.xml
index 62a83bc..14a65a07 100644
--- a/packages/SystemUI/res-keyguard/values-th/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-th/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"ต้องวาดรูปแบบหลังจากอุปกรณ์รีสตาร์ท"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"ต้องระบุ PIN หลังจากอุปกรณ์รีสตาร์ท"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"ต้องป้อนรหัสผ่านหลังจากอุปกรณ์รีสตาร์ท"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"ต้องวาดรูปแบบเพื่อความปลอดภัยเพิ่มเติม"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"ต้องระบุ PIN เพื่อความปลอดภัยเพิ่มเติม"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"ต้องป้อนรหัสผ่านเพื่อความปลอดภัยเพิ่มเติม"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"ใช้รูปแบบแทนเพื่อเพิ่มความปลอดภัย"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"ใช้ PIN แทนเพื่อเพิ่มความปลอดภัย"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"ใช้รหัสผ่านแทนเพื่อเพิ่มความปลอดภัย"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"ผู้ดูแลระบบล็อกอุปกรณ์"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"มีการล็อกอุปกรณ์ด้วยตัวเอง"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"ไม่รู้จัก"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ค่าเริ่มต้น"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"บับเบิล"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"แอนะล็อก"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"ปลดล็อกอุปกรณ์ของคุณเพื่อดำเนินการต่อ"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-tl/strings.xml b/packages/SystemUI/res-keyguard/values-tl/strings.xml
index 524ea47..7936058 100644
--- a/packages/SystemUI/res-keyguard/values-tl/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-tl/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Kailangan ng pattern pagkatapos mag-restart ng device"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Kailangan ng PIN pagkatapos mag-restart ng device"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Kailangan ng password pagkatapos mag-restart ng device"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kinakailangan ang pattern para sa karagdagang seguridad"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kinakailangan ang PIN para sa karagdagang seguridad"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Kinakailangan ang password para sa karagdagang seguridad"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Para sa karagdagang seguridad, gumamit na lang ng pattern"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Para sa karagdagang seguridad, gumamit na lang ng PIN"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Para sa karagdagang seguridad, gumamit na lang ng password"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Na-lock ng admin ang device"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Manual na na-lock ang device"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Hindi nakilala"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Default"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bubble"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"I-unlock ang iyong device para magpatuloy"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-tr/strings.xml b/packages/SystemUI/res-keyguard/values-tr/strings.xml
index 54aaae3..e520762 100644
--- a/packages/SystemUI/res-keyguard/values-tr/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-tr/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Cihaz yeniden başladıktan sonra desen gerekir"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Cihaz yeniden başladıktan sonra PIN gerekir"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Cihaz yeniden başladıktan sonra şifre gerekir"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Ek güvenlik için desen gerekir"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Ek güvenlik için PIN gerekir"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Ek güvenlik için şifre gerekir"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Cihaz, yönetici tarafından kilitlendi"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Cihazın manuel olarak kilitlendi"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Tanınmadı"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Varsayılan"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Baloncuk"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-uk/strings.xml b/packages/SystemUI/res-keyguard/values-uk/strings.xml
index 6144c1c..613181d 100644
--- a/packages/SystemUI/res-keyguard/values-uk/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-uk/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Після перезавантаження пристрою потрібно ввести ключ"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Після перезавантаження пристрою потрібно ввести PIN-код"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Після перезавантаження пристрою потрібно ввести пароль"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Для додаткового захисту потрібно ввести ключ"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Для додаткового захисту потрібно ввести PIN-код"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Для додаткового захисту потрібно ввести пароль"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Адміністратор заблокував пристрій"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Пристрій заблоковано вручну"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Не розпізнано"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"За умовчанням"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Бульбашковий"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Аналоговий"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-ur/strings.xml b/packages/SystemUI/res-keyguard/values-ur/strings.xml
index 4e77841..a122f85 100644
--- a/packages/SystemUI/res-keyguard/values-ur/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-ur/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"آلہ دوبارہ چالو ہونے کے بعد پیٹرن درکار ہوتا ہے"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"‏آلہ دوبارہ چالو ہونے کے بعد PIN درکار ہوتا ہے"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"آلہ دوبارہ چالو ہونے کے بعد پاس ورڈ درکار ہوتا ہے"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"اضافی سیکیورٹی کیلئے پیٹرن درکار ہے"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"‏اضافی سیکیورٹی کیلئے PIN درکار ہے"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"اضافی سیکیورٹی کیلئے پاس ورڈ درکار ہے"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"آلہ منتظم کی جانب سے مقفل ہے"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"آلہ کو دستی طور پر مقفل کیا گیا تھا"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"تسلیم شدہ نہیں ہے"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"ڈیفالٹ"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"بلبلہ"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"اینالاگ"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-uz/strings.xml b/packages/SystemUI/res-keyguard/values-uz/strings.xml
index afaf746..2cc9724 100644
--- a/packages/SystemUI/res-keyguard/values-uz/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-uz/strings.xml
@@ -78,9 +78,9 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Qurilma qayta ishga tushganidan keyin grafik kalitni kiritish zarur"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Qurilma qayta ishga tushganidan keyin PIN kodni kiritish zarur"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Qurilma qayta ishga tushganidan keyin parolni kiritish zarur"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Qo‘shimcha xavfsizlik chorasi sifatida grafik kalit talab qilinadi"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Qo‘shimcha xavfsizlik chorasi sifatida PIN kod talab qilinadi"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Qo‘shimcha xavfsizlik chorasi sifatida parol talab qilinadi"</string>
+    <string name="kg_prompt_reason_timeout_pattern" msgid="5514969660010197363">"Qoʻshimcha xavfsizlik maqsadida oʻrniga grafik kalitdan foydalaning"</string>
+    <string name="kg_prompt_reason_timeout_pin" msgid="4227962059353859376">"Qoʻshimcha xavfsizlik maqsadida oʻrniga PIN koddan foydalaning"</string>
+    <string name="kg_prompt_reason_timeout_password" msgid="8810879144143933690">"Qoʻshimcha xavfsizlik maqsadida oʻrniga paroldan foydalaning"</string>
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Qurilma administrator tomonidan bloklangan"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Qurilma qo‘lda qulflangan"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Aniqlanmadi"</string>
@@ -90,4 +90,5 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Odatiy"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Pufaklar"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Analog"</string>
+    <string name="keyguard_unlock_to_continue" msgid="7509503484250597743">"Davom etish uchun qurilmangizni qulfdan chiqaring"</string>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-vi/strings.xml b/packages/SystemUI/res-keyguard/values-vi/strings.xml
index 1d6cfa8..e7c9295 100644
--- a/packages/SystemUI/res-keyguard/values-vi/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-vi/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Yêu cầu hình mở khóa sau khi thiết bị khởi động lại"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Yêu cầu mã PIN sau khi thiết bị khởi động lại"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Yêu cầu mật khẩu sau khi thiết bị khởi động lại"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Yêu cầu hình mở khóa để bảo mật thêm"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Yêu cầu mã PIN để bảo mật thêm"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Yêu cầu mật khẩu để bảo mật thêm"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Thiết bị đã bị quản trị viên khóa"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Thiết bị đã bị khóa theo cách thủ công"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Không nhận dạng được"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Mặc định"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Bong bóng"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"Đồng hồ kim"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
index 8c8507e..d37d645 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rCN/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"重启设备后需要绘制解锁图案"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"重启设备后需要输入 PIN 码"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"重启设备后需要输入密码"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"需要绘制解锁图案以进一步确保安全"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"需要输入 PIN 码以进一步确保安全"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"需要输入密码以进一步确保安全"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理员已锁定设备"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"此设备已手动锁定"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"无法识别"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"默认"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"指针"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
index c331a92..9dbb8f2 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rHK/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後,必須畫出上鎖圖案才能使用"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後,必須輸入 PIN 碼才能使用"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後,必須輸入密碼才能使用"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請務必畫出上鎖圖案,以進一步確保安全"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請務必輸入 PIN 碼,以進一步確保安全"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請務必輸入密碼,以進一步確保安全"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"裝置已由管理員鎖定"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"使用者已手動將裝置上鎖"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"未能識別"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"預設"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"指針"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
index 1e1bec3..ebb88e1 100644
--- a/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zh-rTW/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"裝置重新啟動後需要畫出解鎖圖案"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"裝置重新啟動後需要輸入 PIN 碼"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"裝置重新啟動後需要輸入密碼"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"請畫出解鎖圖案,以進一步確保資訊安全"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"請輸入 PIN 碼,以進一步確保資訊安全"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"請輸入密碼,以進一步確保資訊安全"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"管理員已鎖定裝置"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"裝置已手動鎖定"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"無法識別"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"預設"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"泡泡"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"類比"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values-zu/strings.xml b/packages/SystemUI/res-keyguard/values-zu/strings.xml
index c8f78ea..57e56f7 100644
--- a/packages/SystemUI/res-keyguard/values-zu/strings.xml
+++ b/packages/SystemUI/res-keyguard/values-zu/strings.xml
@@ -78,9 +78,12 @@
     <string name="kg_prompt_reason_restart_pattern" msgid="4720554342633852066">"Iphethini iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
     <string name="kg_prompt_reason_restart_pin" msgid="1587671566498057656">"Iphinikhodi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
     <string name="kg_prompt_reason_restart_password" msgid="8061279087240952002">"Iphasiwedi iyadingeka ngemuva kokuqala kabusha kwedivayisi"</string>
-    <string name="kg_prompt_reason_timeout_pattern" msgid="9170360502528959889">"Kudingeka iphethini  ngokuvikeleka okungeziwe"</string>
-    <string name="kg_prompt_reason_timeout_pin" msgid="5945186097160029201">"Kudingeka iphinikhodi ngokuvikeleka okungeziwe"</string>
-    <string name="kg_prompt_reason_timeout_password" msgid="2258263949430384278">"Iphasiwedi idingelwa ukuvikela okungeziwe"</string>
+    <!-- no translation found for kg_prompt_reason_timeout_pattern (5514969660010197363) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_pin (4227962059353859376) -->
+    <skip />
+    <!-- no translation found for kg_prompt_reason_timeout_password (8810879144143933690) -->
+    <skip />
     <string name="kg_prompt_reason_device_admin" msgid="6961159596224055685">"Idivayisi ikhiywe ngumlawuli"</string>
     <string name="kg_prompt_reason_user_request" msgid="6015774877733717904">"Idivayisi ikhiywe ngokwenza"</string>
     <string name="kg_face_not_recognized" msgid="7903950626744419160">"Akwaziwa"</string>
@@ -90,4 +93,6 @@
     <string name="clock_title_default" msgid="6342735240617459864">"Okuzenzekelayo"</string>
     <string name="clock_title_bubble" msgid="2204559396790593213">"Ibhamuza"</string>
     <string name="clock_title_analog" msgid="8409262532900918273">"I-Analog"</string>
+    <!-- no translation found for keyguard_unlock_to_continue (7509503484250597743) -->
+    <skip />
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml
index b1d3375..a25ab51 100644
--- a/packages/SystemUI/res-keyguard/values/config.xml
+++ b/packages/SystemUI/res-keyguard/values/config.xml
@@ -28,11 +28,6 @@
     <!-- Will display the bouncer on one side of the display, and the current user icon and
          user switcher on the other side -->
     <bool name="config_enableBouncerUserSwitcher">false</bool>
-    <!-- Whether to show the face scanning animation on devices with face auth supported.
-         The face scanning animation renders in a SW layer in ScreenDecorations.
-         Enabling this will also render the camera protection in the SW layer
-         (instead of HW, if relevant)."=-->
-    <bool name="config_enableFaceScanningAnimation">true</bool>
     <!-- Time to be considered a consecutive fingerprint failure in ms -->
     <integer name="fp_consecutive_failure_time_ms">3500</integer>
 </resources>
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 46f6ab2..0a55cf7 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -119,6 +119,7 @@
     <dimen name="bouncer_user_switcher_width">248dp</dimen>
     <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen>
     <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen>
+    <dimen name="bouncer_user_switcher_popup_items_divider_height">2dp</dimen>
     <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen>
     <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen>
     <dimen name="bouncer_user_switcher_header_padding_end">44dp</dimen>
diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res/drawable/media_squiggly_progress.xml
similarity index 91%
rename from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
rename to packages/SystemUI/res/drawable/media_squiggly_progress.xml
index 9e61236..9cd3f62 100644
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ b/packages/SystemUI/res/drawable/media_squiggly_progress.xml
@@ -14,4 +14,4 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file
+<com.android.systemui.media.controls.ui.SquigglyProgress />
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml
similarity index 67%
copy from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
copy to packages/SystemUI/res/drawable/overlay_badge_background.xml
index 9e61236..857632e 100644
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2022 The Android Open Source Project
+  ~ Copyright (C) 2020 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -14,4 +14,8 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        android:shape="oval">
+    <solid android:color="?androidprv:attr/colorSurface"/>
+</shape>
diff --git a/packages/SystemUI/res/drawable/qs_media_background.xml b/packages/SystemUI/res/drawable/qs_media_background.xml
index 6ed3a0ae..217656da 100644
--- a/packages/SystemUI/res/drawable/qs_media_background.xml
+++ b/packages/SystemUI/res/drawable/qs_media_background.xml
@@ -14,7 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<com.android.systemui.media.IlluminationDrawable
+<com.android.systemui.media.controls.ui.IlluminationDrawable
     xmlns:systemui="http://schemas.android.com/apk/res-auto"
     systemui:highlight="15"
     systemui:cornerRadius="@dimen/notification_corner_radius" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_media_light_source.xml b/packages/SystemUI/res/drawable/qs_media_light_source.xml
index b2647c1..849349a 100644
--- a/packages/SystemUI/res/drawable/qs_media_light_source.xml
+++ b/packages/SystemUI/res/drawable/qs_media_light_source.xml
@@ -14,7 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.media.LightSourceDrawable
+<com.android.systemui.media.controls.ui.LightSourceDrawable
     xmlns:systemui="http://schemas.android.com/apk/res-auto"
     systemui:rippleMinSize="25dp"
     systemui:rippleMaxSize="135dp" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
index bc8e540..3bcc37a 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml
@@ -16,45 +16,47 @@
 
 <com.android.systemui.biometrics.AuthCredentialPasswordView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPinPasswordStyle">
 
     <RelativeLayout
         android:id="@+id/auth_credential_header"
-        style="@style/AuthCredentialHeaderStyle"
+        style="?headerStyle"
         android:layout_width="wrap_content"
         android:layout_height="match_parent">
 
         <ImageView
             android:id="@+id/icon"
-            style="@style/TextAppearance.AuthNonBioCredential.Icon"
+            style="?headerIconStyle"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
             android:contentDescription="@null"/>
 
         <TextView
             android:id="@+id/title"
-            style="@style/TextAppearance.AuthNonBioCredential.Title"
+            style="?titleTextAppearance"
             android:layout_below="@id/icon"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/subtitle"
-            style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
+            style="?subTitleTextAppearance"
             android:layout_below="@id/title"
             android:layout_alignParentLeft="true"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/description"
-            style="@style/TextAppearance.AuthNonBioCredential.Description"
+            style="?descriptionTextAppearance"
             android:layout_below="@id/subtitle"
             android:layout_alignParentLeft="true"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
     </RelativeLayout>
@@ -67,7 +69,7 @@
 
         <ImeAwareEditText
             android:id="@+id/lockPassword"
-            style="@style/TextAppearance.AuthCredential.PasswordEntry"
+            style="?passwordTextAppearance"
             android:layout_width="208dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
@@ -77,7 +79,7 @@
 
         <TextView
             android:id="@+id/error"
-            style="@style/TextAppearance.AuthNonBioCredential.Error"
+            style="?errorTextAppearance"
             android:layout_gravity="center"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content" />
diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
index 19a85fe..a3dd334 100644
--- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml
@@ -16,91 +16,71 @@
 
 <com.android.systemui.biometrics.AuthCredentialPatternView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPatternStyle">
 
-    <LinearLayout
+    <RelativeLayout
+        android:id="@+id/auth_credential_header"
+        style="?headerStyle"
         android:layout_width="0dp"
         android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:gravity="center"
-        android:orientation="vertical">
-
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+        android:layout_weight="1">
 
         <ImageView
             android:id="@+id/icon"
+            style="?headerIconStyle"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:contentDescription="@null"/>
+
+        <TextView
+            android:id="@+id/title"
+            style="?titleTextAppearance"
+            android:layout_below="@id/icon"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 
         <TextView
-            android:id="@+id/title"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Title"/>
-
-        <TextView
             android:id="@+id/subtitle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Subtitle"/>
+            style="?subTitleTextAppearance"
+            android:layout_below="@id/title"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content" />
 
         <TextView
             android:id="@+id/description"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Description"/>
+            style="?descriptionTextAppearance"
+            android:layout_below="@id/subtitle"
+            android:layout_alignParentLeft="true"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
+    </RelativeLayout>
+
+    <FrameLayout
+        android:layout_weight="1"
+        style="?containerStyle"
+        android:layout_width="0dp"
+        android:layout_height="match_parent">
+
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPattern"
+            android:layout_gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
 
         <TextView
             android:id="@+id/error"
+            style="?errorTextAppearance"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            style="@style/TextAppearance.AuthCredential.Error"/>
+            android:layout_gravity="center_horizontal|bottom"/>
 
-        <Space
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:gravity="center"
-        android:paddingLeft="0dp"
-        android:paddingRight="0dp"
-        android:paddingTop="0dp"
-        android:paddingBottom="16dp"
-        android:clipToPadding="false">
-
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="0dp"
-            android:layout_weight="1"
-            style="@style/LockPatternContainerStyle">
-
-            <com.android.internal.widget.LockPatternView
-                android:id="@+id/lockPattern"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:layout_gravity="center"
-                style="@style/LockPatternStyleBiometricPrompt"/>
-
-        </FrameLayout>
-
-    </LinearLayout>
+    </FrameLayout>
 
 </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml
index 75a80bc..774b335f 100644
--- a/packages/SystemUI/res/layout/auth_credential_password_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml
@@ -16,43 +16,45 @@
 
 <com.android.systemui.biometrics.AuthCredentialPasswordView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:elevation="@dimen/biometric_dialog_elevation"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:theme="?app:attr/lockPinPasswordStyle">
 
     <RelativeLayout
         android:id="@+id/auth_credential_header"
-        style="@style/AuthCredentialHeaderStyle"
+        style="?headerStyle"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
         <ImageView
             android:id="@+id/icon"
-            style="@style/TextAppearance.AuthNonBioCredential.Icon"
+            style="?headerIconStyle"
             android:layout_alignParentLeft="true"
             android:layout_alignParentTop="true"
             android:contentDescription="@null"/>
 
         <TextView
             android:id="@+id/title"
-            style="@style/TextAppearance.AuthNonBioCredential.Title"
+            style="?titleTextAppearance"
             android:layout_below="@id/icon"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
         <TextView
             android:id="@+id/subtitle"
-            style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
+            style="?subTitleTextAppearance"
             android:layout_below="@id/title"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
 
         <TextView
             android:id="@+id/description"
-            style="@style/TextAppearance.AuthNonBioCredential.Description"
+            style="?descriptionTextAppearance"
             android:layout_below="@id/subtitle"
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"/>
     </RelativeLayout>
 
@@ -64,7 +66,7 @@
 
         <ImeAwareEditText
             android:id="@+id/lockPassword"
-            style="@style/TextAppearance.AuthCredential.PasswordEntry"
+            style="?passwordTextAppearance"
             android:layout_width="208dp"
             android:layout_height="wrap_content"
             android:layout_gravity="center_horizontal"
@@ -74,7 +76,7 @@
 
         <TextView
             android:id="@+id/error"
-            style="@style/TextAppearance.AuthNonBioCredential.Error"
+            style="?errorTextAppearance"
             android:layout_gravity="center_horizontal"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
index dada981..4af9970 100644
--- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
+++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml
@@ -16,87 +16,66 @@
 
 <com.android.systemui.biometrics.AuthCredentialPatternView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:gravity="center_horizontal"
-    android:elevation="@dimen/biometric_dialog_elevation">
+    android:elevation="@dimen/biometric_dialog_elevation"
+    android:theme="?app:attr/lockPatternStyle">
 
     <RelativeLayout
+        android:id="@+id/auth_credential_header"
+        style="?headerStyle"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
+        android:layout_height="wrap_content">
 
-        <LinearLayout
-            android:id="@+id/auth_credential_header"
-            style="@style/AuthCredentialHeaderStyle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+        <ImageView
+            android:id="@+id/icon"
+            style="?headerIconStyle"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:contentDescription="@null"/>
 
-            <ImageView
-                android:id="@+id/icon"
-                android:layout_width="48dp"
-                android:layout_height="48dp"
-                android:contentDescription="@null" />
+        <TextView
+            android:id="@+id/title"
+            style="?titleTextAppearance"
+            android:layout_below="@id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/title"
-                style="@style/TextAppearance.AuthNonBioCredential.Title"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
+        <TextView
+            android:id="@+id/subtitle"
+            style="?subTitleTextAppearance"
+            android:layout_below="@id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/subtitle"
-                style="@style/TextAppearance.AuthNonBioCredential.Subtitle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-
-            <TextView
-                android:id="@+id/description"
-                style="@style/TextAppearance.AuthNonBioCredential.Description"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content" />
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_below="@id/auth_credential_header"
-            android:gravity="center"
-            android:orientation="vertical"
-            android:paddingBottom="16dp"
-            android:paddingTop="60dp">
-
-            <FrameLayout
-                style="@style/LockPatternContainerStyle"
-                android:layout_width="wrap_content"
-                android:layout_height="0dp"
-                android:layout_weight="1">
-
-                <com.android.internal.widget.LockPatternView
-                    android:id="@+id/lockPattern"
-                    style="@style/LockPatternStyle"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:layout_gravity="center" />
-
-            </FrameLayout>
-
-        </LinearLayout>
-
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_alignParentBottom="true">
-
-            <TextView
-                android:id="@+id/error"
-                style="@style/TextAppearance.AuthNonBioCredential.Error"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-        </LinearLayout>
-
+        <TextView
+            android:id="@+id/description"
+            style="?descriptionTextAppearance"
+            android:layout_below="@id/subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
     </RelativeLayout>
 
+    <FrameLayout
+        android:id="@+id/auth_credential_container"
+        style="?containerStyle"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.android.internal.widget.LockPatternView
+            android:id="@+id/lockPattern"
+            android:layout_gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+
+        <TextView
+            android:id="@+id/error"
+            style="?errorTextAppearance"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal|bottom"/>
+    </FrameLayout>
+
 </com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml
index 4da7711..bc97e51 100644
--- a/packages/SystemUI/res/layout/chipbar.xml
+++ b/packages/SystemUI/res/layout/chipbar.xml
@@ -19,12 +19,12 @@
 <com.android.systemui.temporarydisplay.chipbar.ChipbarRootView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/media_ttt_sender_chip"
+    android:id="@+id/chipbar_root_view"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
     <LinearLayout
-        android:id="@+id/media_ttt_sender_chip_inner"
+        android:id="@+id/chipbar_inner"
         android:orientation="horizontal"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -39,7 +39,7 @@
         >
 
         <com.android.internal.widget.CachingIconView
-            android:id="@+id/app_icon"
+            android:id="@+id/start_icon"
             android:layout_width="@dimen/media_ttt_app_icon_size"
             android:layout_height="@dimen/media_ttt_app_icon_size"
             android:layout_marginEnd="12dp"
@@ -69,7 +69,7 @@
             />
 
         <ImageView
-            android:id="@+id/failure_icon"
+            android:id="@+id/error"
             android:layout_width="@dimen/media_ttt_status_icon_size"
             android:layout_height="@dimen/media_ttt_status_icon_size"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
@@ -78,11 +78,11 @@
             android:alpha="0.0"
             />
 
+        <!-- TODO(b/245610654): Re-name all the media-specific dimens to chipbar dimens instead. -->
         <TextView
-            android:id="@+id/undo"
+            android:id="@+id/end_button"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="@string/media_transfer_undo"
             android:textColor="?androidprv:attr/textColorOnAccent"
             android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
             android:textSize="@dimen/media_ttt_text_size"
diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml
index 50d3cc4..715c869 100644
--- a/packages/SystemUI/res/layout/media_carousel.xml
+++ b/packages/SystemUI/res/layout/media_carousel.xml
@@ -24,7 +24,7 @@
     android:clipToPadding="false"
     android:forceHasOverlappingRendering="false"
     android:theme="@style/MediaPlayer">
-    <com.android.systemui.media.MediaScrollView
+    <com.android.systemui.media.controls.ui.MediaScrollView
         android:id="@+id/media_carousel_scroller"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -42,7 +42,7 @@
             >
             <!-- QSMediaPlayers will be added here dynamically -->
         </LinearLayout>
-    </com.android.systemui.media.MediaScrollView>
+    </com.android.systemui.media.controls.ui.MediaScrollView>
     <com.android.systemui.qs.PageIndicator
         android:id="@+id/media_page_indicator"
         android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 2fb6d6c..9fc3f40 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -23,6 +23,7 @@
     android:layout_height="wrap_content"
     android:layout_gravity="@integer/notification_panel_layout_gravity"
     android:background="@android:color/transparent"
+    android:importantForAccessibility="no"
     android:baselineAligned="false"
     android:clickable="false"
     android:clipChildren="false"
@@ -56,7 +57,7 @@
             android:clipToPadding="false"
             android:focusable="true"
             android:paddingBottom="@dimen/qqs_layout_padding_bottom"
-            android:importantForAccessibility="yes">
+            android:importantForAccessibility="no">
         </com.android.systemui.qs.QuickQSPanel>
     </RelativeLayout>
 
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
index 60bc373..8b5d953 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
@@ -25,6 +25,7 @@
     android:gravity="center"
     android:layout_gravity="top"
     android:orientation="horizontal"
+    android:importantForAccessibility="no"
     android:clickable="true"
     android:minHeight="48dp">
 
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 9c02749..1ac78d4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -103,8 +103,18 @@
         app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
         app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
-        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border">
-    </ImageView>
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/>
+    <ImageView
+        android:id="@+id/screenshot_badge"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:padding="4dp"
+        android:visibility="gone"
+        android:background="@drawable/overlay_badge_background"
+        android:elevation="8dp"
+        android:src="@drawable/overlay_cancel"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
     <FrameLayout
         android:id="@+id/screenshot_dismiss_button"
         android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index f0e49d5..159323a 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -32,41 +32,8 @@
         android:layout_height="match_parent"
         android:layout_width="match_parent" />
 
-    <include
-        layout="@layout/keyguard_bottom_area"
-        android:visibility="gone" />
-
-    <ViewStub
-        android:id="@+id/keyguard_user_switcher_stub"
-        android:layout="@layout/keyguard_user_switcher"
-        android:layout_height="match_parent"
-        android:layout_width="match_parent" />
-
     <include layout="@layout/status_bar_expanded_plugin_frame"/>
 
-    <include layout="@layout/dock_info_bottom_area_overlay" />
-
-    <com.android.keyguard.LockIconView
-        android:id="@+id/lock_icon_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content">
-        <!-- Background protection -->
-        <ImageView
-            android:id="@+id/lock_icon_bg"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="@drawable/fingerprint_bg"
-            android:visibility="invisible"/>
-
-        <ImageView
-            android:id="@+id/lock_icon"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_gravity="center"
-            android:scaleType="centerCrop"/>
-
-    </com.android.keyguard.LockIconView>
-
     <com.android.systemui.shade.NotificationsQuickSettingsContainer
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -75,12 +42,6 @@
         android:clipToPadding="false"
         android:clipChildren="false">
 
-        <ViewStub
-            android:id="@+id/qs_header_stub"
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-        />
-
         <include
             layout="@layout/keyguard_status_view"
             android:visibility="gone"/>
@@ -102,6 +63,15 @@
             systemui:layout_constraintBottom_toBottomOf="parent"
         />
 
+        <!-- This view should be after qs_frame so touches are dispatched first to it. That gives
+             it a chance to capture clicks before the NonInterceptingScrollView disallows all
+             intercepts -->
+        <ViewStub
+            android:id="@+id/qs_header_stub"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+        />
+
         <androidx.constraintlayout.widget.Guideline
             android:id="@+id/qs_edge_guideline"
             android:layout_width="wrap_content"
@@ -145,6 +115,39 @@
         />
     </com.android.systemui.shade.NotificationsQuickSettingsContainer>
 
+    <include
+        layout="@layout/keyguard_bottom_area"
+        android:visibility="gone" />
+
+    <ViewStub
+        android:id="@+id/keyguard_user_switcher_stub"
+        android:layout="@layout/keyguard_user_switcher"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
+    <include layout="@layout/dock_info_bottom_area_overlay" />
+
+    <com.android.keyguard.LockIconView
+        android:id="@+id/lock_icon_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <!-- Background protection -->
+        <ImageView
+            android:id="@+id/lock_icon_bg"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/fingerprint_bg"
+            android:visibility="invisible"/>
+
+        <ImageView
+            android:id="@+id/lock_icon"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:scaleType="centerCrop"/>
+
+    </com.android.keyguard.LockIconView>
+
     <FrameLayout
         android:id="@+id/preview_container"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index ac9a947..aefd998 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -24,7 +24,36 @@
         <item name="android:paddingEnd">24dp</item>
         <item name="android:paddingTop">48dp</item>
         <item name="android:paddingBottom">10dp</item>
-        <item name="android:gravity">top|center_horizontal</item>
+        <item name="android:gravity">top|left</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">320dp</item>
+        <item name="android:maxWidth">320dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">60dp</item>
+        <item name="android:paddingVertical">20dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">36dp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">6dp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp-land/styles.xml b/packages/SystemUI/res/values-sw600dp-land/styles.xml
new file mode 100644
index 0000000..8148d3d
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-land/styles.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">120dp</item>
+        <item name="android:paddingVertical">40dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-port/styles.xml b/packages/SystemUI/res/values-sw600dp-port/styles.xml
new file mode 100644
index 0000000..771de08
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-port/styles.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">120dp</item>
+        <item name="android:paddingEnd">120dp</item>
+        <item name="android:paddingTop">80dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">180dp</item>
+        <item name="android:paddingVertical">80dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-land/styles.xml b/packages/SystemUI/res/values-sw720dp-land/styles.xml
new file mode 100644
index 0000000..f9ed67d
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-land/styles.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">120dp</item>
+        <item name="android:paddingVertical">40dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Subtitle">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Description">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">16dp</item>
+        <item name="android:textSize">18sp</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/styles.xml b/packages/SystemUI/res/values-sw720dp-port/styles.xml
new file mode 100644
index 0000000..78d299c
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-port/styles.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="AuthCredentialHeaderStyle">
+        <item name="android:paddingStart">120dp</item>
+        <item name="android:paddingEnd">120dp</item>
+        <item name="android:paddingTop">80dp</item>
+        <item name="android:paddingBottom">10dp</item>
+        <item name="android:layout_gravity">top</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:paddingHorizontal">240dp</item>
+        <item name="android:paddingVertical">120dp</item>
+    </style>
+
+    <style name="TextAppearance.AuthNonBioCredential.Title">
+        <item name="android:fontFamily">google-sans</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:focusable">true</item>
+    </style>
+
+</resources>
diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml
index 5f1863a..8a1a9e2 100644
--- a/packages/SystemUI/res/values-zh-rTW/strings.xml
+++ b/packages/SystemUI/res/values-zh-rTW/strings.xml
@@ -672,7 +672,7 @@
     <string name="data_connection_no_internet" msgid="691058178914184544">"沒有網際網路連線"</string>
     <string name="accessibility_quick_settings_open_settings" msgid="536838345505030893">"開啟「<xliff:g id="ID_1">%s</xliff:g>」設定。"</string>
     <string name="accessibility_quick_settings_edit" msgid="1523745183383815910">"編輯設定順序。"</string>
-    <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源按鈕選單"</string>
+    <string name="accessibility_quick_settings_power_menu" msgid="6820426108301758412">"電源鍵選單"</string>
     <string name="accessibility_quick_settings_page" msgid="7506322631645550961">"第 <xliff:g id="ID_1">%1$d</xliff:g> 頁,共 <xliff:g id="ID_2">%2$d</xliff:g> 頁"</string>
     <string name="tuner_lock_screen" msgid="2267383813241144544">"鎖定畫面"</string>
     <string name="thermal_shutdown_title" msgid="2702966892682930264">"手機先前過熱,因此關閉電源"</string>
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 9a71995..df0659d 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -191,5 +191,18 @@
     <declare-styleable name="DelayableMarqueeTextView">
         <attr name="marqueeDelay" format="integer" />
     </declare-styleable>
+
+    <declare-styleable name="AuthCredentialView">
+        <attr name="lockPatternStyle" format="reference" />
+        <attr name="lockPinPasswordStyle" format="reference" />
+        <attr name="containerStyle" format="reference" />
+        <attr name="headerStyle" format="reference" />
+        <attr name="headerIconStyle" format="reference" />
+        <attr name="titleTextAppearance" format="reference" />
+        <attr name="subTitleTextAppearance" format="reference" />
+        <attr name="descriptionTextAppearance" format="reference" />
+        <attr name="passwordTextAppearance" format="reference" />
+        <attr name="errorTextAppearance" format="reference"/>
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 637ac19..d4d8843 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -641,7 +641,7 @@
     <!-- QuickSettings: Label for the toggle that controls whether display color correction is enabled. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_color_correction_label">Color correction</string>
     <!-- QuickSettings: Control panel: Label for button that navigates to user settings. [CHAR LIMIT=NONE] -->
-    <string name="quick_settings_more_user_settings">User settings</string>
+    <string name="quick_settings_more_user_settings">Manage users</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses control panel. [CHAR LIMIT=NONE] -->
     <string name="quick_settings_done">Done</string>
     <!-- QuickSettings: Control panel: Label for button that dismisses user switcher control panel. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 475ca91..e76887b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -195,15 +195,11 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
-    <style name="TextAppearance.AuthNonBioCredential.Icon">
-        <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
-        <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
-    </style>
-
     <style name="TextAppearance.AuthNonBioCredential.Title">
         <item name="android:fontFamily">google-sans</item>
-        <item name="android:layout_marginTop">20dp</item>
-        <item name="android:textSize">36sp</item>
+        <item name="android:layout_marginTop">24dp</item>
+        <item name="android:textSize">36dp</item>
+        <item name="android:focusable">true</item>
     </style>
 
     <style name="TextAppearance.AuthNonBioCredential.Subtitle">
@@ -215,12 +211,10 @@
     <style name="TextAppearance.AuthNonBioCredential.Description">
         <item name="android:fontFamily">google-sans</item>
         <item name="android:layout_marginTop">20dp</item>
-        <item name="android:textSize">16sp</item>
+        <item name="android:textSize">18sp</item>
     </style>
 
     <style name="TextAppearance.AuthNonBioCredential.Error">
-        <item name="android:paddingTop">6dp</item>
-        <item name="android:paddingBottom">18dp</item>
         <item name="android:paddingHorizontal">24dp</item>
         <item name="android:textSize">14sp</item>
         <item name="android:textColor">?android:attr/colorError</item>
@@ -239,12 +233,33 @@
     <style name="AuthCredentialHeaderStyle">
         <item name="android:paddingStart">48dp</item>
         <item name="android:paddingEnd">48dp</item>
-        <item name="android:paddingTop">28dp</item>
-        <item name="android:paddingBottom">20dp</item>
-        <item name="android:orientation">vertical</item>
+        <item name="android:paddingTop">48dp</item>
+        <item name="android:paddingBottom">10dp</item>
         <item name="android:layout_gravity">top</item>
     </style>
 
+    <style name="AuthCredentialIconStyle">
+        <item name="android:layout_width">@dimen/biometric_auth_icon_size</item>
+        <item name="android:layout_height">@dimen/biometric_auth_icon_size</item>
+    </style>
+
+    <style name="AuthCredentialPatternContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">420dp</item>
+        <item name="android:maxWidth">420dp</item>
+        <item name="android:minHeight">200dp</item>
+        <item name="android:minWidth">200dp</item>
+        <item name="android:padding">20dp</item>
+    </style>
+
+    <style name="AuthCredentialPinPasswordContainerStyle">
+        <item name="android:gravity">center</item>
+        <item name="android:maxHeight">48dp</item>
+        <item name="android:maxWidth">600dp</item>
+        <item name="android:minHeight">48dp</item>
+        <item name="android:minWidth">200dp</item>
+    </style>
+
     <style name="DeviceManagementDialogTitle">
         <item name="android:gravity">center</item>
         <item name="android:textAppearance">@style/TextAppearance.DeviceManagementDialog.Title</item>
@@ -282,7 +297,9 @@
         <item name="wallpaperTextColorSecondary">@*android:color/secondary_text_material_dark</item>
         <item name="wallpaperTextColorAccent">@color/material_dynamic_primary90</item>
         <item name="android:colorError">@*android:color/error_color_material_dark</item>
-        <item name="*android:lockPatternStyle">@style/LockPatternStyle</item>
+        <item name="*android:lockPatternStyle">@style/LockPatternViewStyle</item>
+        <item name="lockPatternStyle">@style/LockPatternContainerStyle</item>
+        <item name="lockPinPasswordStyle">@style/LockPinPasswordContainerStyle</item>
         <item name="passwordStyle">@style/PasswordTheme</item>
         <item name="numPadKeyStyle">@style/NumPadKey</item>
         <item name="backgroundProtectedStyle">@style/BackgroundProtectedStyle</item>
@@ -308,27 +325,33 @@
         <item name="android:textColor">?attr/wallpaperTextColor</item>
     </style>
 
-    <style name="LockPatternContainerStyle">
-        <item name="android:maxHeight">400dp</item>
-        <item name="android:maxWidth">420dp</item>
-        <item name="android:minHeight">0dp</item>
-        <item name="android:minWidth">0dp</item>
-        <item name="android:paddingHorizontal">60dp</item>
-        <item name="android:paddingBottom">40dp</item>
+    <style name="AuthCredentialStyle">
+        <item name="*android:regularColor">?android:attr/colorForeground</item>
+        <item name="*android:successColor">?android:attr/colorForeground</item>
+        <item name="*android:errorColor">?android:attr/colorError</item>
+        <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+        <item name="headerStyle">@style/AuthCredentialHeaderStyle</item>
+        <item name="headerIconStyle">@style/AuthCredentialIconStyle</item>
+        <item name="titleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Title</item>
+        <item name="subTitleTextAppearance">@style/TextAppearance.AuthNonBioCredential.Subtitle</item>
+        <item name="descriptionTextAppearance">@style/TextAppearance.AuthNonBioCredential.Description</item>
+        <item name="passwordTextAppearance">@style/TextAppearance.AuthCredential.PasswordEntry</item>
+        <item name="errorTextAppearance">@style/TextAppearance.AuthNonBioCredential.Error</item>
     </style>
 
-    <style name="LockPatternStyle">
+    <style name="LockPatternViewStyle" >
         <item name="*android:regularColor">?android:attr/colorAccent</item>
         <item name="*android:successColor">?android:attr/textColorPrimary</item>
         <item name="*android:errorColor">?android:attr/colorError</item>
         <item name="*android:dotColor">?android:attr/textColorSecondary</item>
     </style>
 
-    <style name="LockPatternStyleBiometricPrompt">
-        <item name="*android:regularColor">?android:attr/colorForeground</item>
-        <item name="*android:successColor">?android:attr/colorForeground</item>
-        <item name="*android:errorColor">?android:attr/colorError</item>
-        <item name="*android:dotColor">?android:attr/textColorSecondary</item>
+    <style name="LockPatternContainerStyle" parent="@style/AuthCredentialStyle">
+        <item name="containerStyle">@style/AuthCredentialPatternContainerStyle</item>
+    </style>
+
+    <style name="LockPinPasswordContainerStyle" parent="@style/AuthCredentialStyle">
+        <item name="containerStyle">@style/AuthCredentialPinPasswordContainerStyle</item>
     </style>
 
     <style name="Theme.SystemUI.QuickSettings" parent="@*android:style/Theme.DeviceDefault">
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
index 2e391c7..49cc483 100644
--- a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ExternalViewScreenshotTestRule.kt
@@ -19,6 +19,7 @@
 import android.app.Activity
 import android.graphics.Color
 import android.view.View
+import android.view.Window
 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
@@ -51,13 +52,14 @@
 
     /**
      * Compare the content of the [view] with the golden image identified by [goldenIdentifier] in
-     * the context of [emulationSpec].
+     * the context of [emulationSpec]. Window must be specified to capture views that render
+     * hardware buffers.
      */
-    fun screenshotTest(goldenIdentifier: String, view: View) {
+    fun screenshotTest(goldenIdentifier: String, view: View, window: Window? = null) {
         view.removeElevationRecursively()
 
         ScreenshotRuleAsserter.Builder(screenshotRule)
-            .setScreenshotProvider { view.toBitmap() }
+            .setScreenshotProvider { view.toBitmap(window) }
             .withMatcher(matcher)
             .build()
             .assertGoldenImage(goldenIdentifier)
@@ -94,6 +96,6 @@
             activity.currentFocus?.clearFocus()
         }
 
-        screenshotTest(goldenIdentifier, rootView)
+        screenshotTest(goldenIdentifier, rootView, activity.window)
     }
 }
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 9040ea1..ffaeaaa 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -62,7 +62,6 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
-    java_version: "1.8",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 134f3bc..236aa66 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -33,6 +33,8 @@
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Calendar
@@ -52,14 +54,8 @@
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-
-    private var lastMeasureCall: CharSequence? = null
-    private var lastDraw: CharSequence? = null
-    private var lastTextUpdate: CharSequence? = null
-    private var lastOnTextChanged: CharSequence? = null
-    private var lastInvalidate: CharSequence? = null
-    private var lastTimeZoneChange: CharSequence? = null
-    private var lastAnimationCall: CharSequence? = null
+    var tag: String = "UnnamedClockView"
+    var logBuffer: LogBuffer? = null
 
     private val time = Calendar.getInstance()
 
@@ -136,6 +132,7 @@
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
+        logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
         refreshFormat()
     }
 
@@ -151,27 +148,39 @@
         time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
+        logBuffer?.log(tag, DEBUG,
+                { str1 = formattedText?.toString() },
+                { "refreshTime: new formattedText=$str1" }
+        )
         // 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
+            logBuffer?.log(tag, DEBUG,
+                    { str1 = formattedText?.toString() },
+                    { "refreshTime: done setting new time text to: $str1" }
+            )
             // 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)
+                logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
             }
             requestLayout()
-            lastTextUpdate = getTimestamp()
+            logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
         }
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
         time.timeZone = timeZone
         refreshFormat()
-        lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}"
+        logBuffer?.log(tag, DEBUG,
+                { str1 = timeZone?.toString() },
+                { "onTimeZoneChanged newTimeZone=$str1" }
+        )
     }
 
     @SuppressLint("DrawAllocation")
@@ -185,22 +194,24 @@
         } else {
             animator.updateLayout(layout)
         }
-        lastMeasureCall = getTimestamp()
+        logBuffer?.log(tag, DEBUG, "onMeasure")
     }
 
     override fun onDraw(canvas: Canvas) {
-        lastDraw = getTimestamp()
-        // intentionally doesn't call super.onDraw here or else the text will be rendered twice
-        textAnimator?.draw(canvas)
+        // Use textAnimator to render text if animation is enabled.
+        // Otherwise default to using standard draw functions.
+        if (isAnimationEnabled) {
+            // intentionally doesn't call super.onDraw here or else the text will be rendered twice
+            textAnimator?.draw(canvas)
+        } else {
+            super.onDraw(canvas)
+        }
+        logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
     }
 
     override fun invalidate() {
         super.invalidate()
-        lastInvalidate = getTimestamp()
-    }
-
-    private fun getTimestamp(): CharSequence {
-        return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text"
+        logBuffer?.log(tag, DEBUG, "invalidate")
     }
 
     override fun onTextChanged(
@@ -210,7 +221,10 @@
             lengthAfter: Int
     ) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
-        lastOnTextChanged = "${getTimestamp()}"
+        logBuffer?.log(tag, DEBUG,
+                { str1 = text.toString() },
+                { "onTextChanged text=$str1" }
+        )
     }
 
     fun setLineSpacingScale(scale: Float) {
@@ -224,7 +238,7 @@
     }
 
     fun animateAppearOnLockscreen() {
-        lastAnimationCall = "${getTimestamp()} call=animateAppearOnLockscreen"
+        logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
         setTextStyle(
             weight = dozingWeight,
             textSize = -1f,
@@ -249,7 +263,7 @@
         if (isAnimationEnabled && textAnimator == null) {
             return
         }
-        lastAnimationCall = "${getTimestamp()} call=animateFoldAppear"
+        logBuffer?.log(tag, DEBUG, "animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
             textSize = -1f,
@@ -276,7 +290,7 @@
             // Skip charge animation if dozing animation is already playing.
             return
         }
-        lastAnimationCall = "${getTimestamp()} call=animateCharge"
+        logBuffer?.log(tag, DEBUG, "animateCharge")
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -300,7 +314,7 @@
     }
 
     fun animateDoze(isDozing: Boolean, animate: Boolean) {
-        lastAnimationCall = "${getTimestamp()} call=animateDoze"
+        logBuffer?.log(tag, DEBUG, "animateDoze")
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
             textSize = -1f,
@@ -363,6 +377,9 @@
                 onAnimationEnd = onAnimationEnd
             )
             textAnimator?.glyphFilter = glyphFilter
+            if (color != null && !isAnimationEnabled) {
+                setTextColor(color)
+            }
         } else {
             // when the text animator is set, update its start values
             onTextAnimatorInitialized = Runnable {
@@ -377,6 +394,9 @@
                     onAnimationEnd = onAnimationEnd
                 )
                 textAnimator?.glyphFilter = glyphFilter
+                if (color != null && !isAnimationEnabled) {
+                    setTextColor(color)
+                }
             }
         }
     }
@@ -412,9 +432,12 @@
             isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
             else -> DOUBLE_LINE_FORMAT_12_HOUR
         }
+        logBuffer?.log(tag, DEBUG,
+                { str1 = format?.toString() },
+                { "refreshFormat format=$str1" }
+        )
 
         descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
-
         refreshTime()
     }
 
@@ -423,15 +446,8 @@
         pw.println("    measuredWidth=$measuredWidth")
         pw.println("    measuredHeight=$measuredHeight")
         pw.println("    singleLineInternal=$isSingleLineInternal")
-        pw.println("    lastTextUpdate=$lastTextUpdate")
-        pw.println("    lastOnTextChanged=$lastOnTextChanged")
-        pw.println("    lastInvalidate=$lastInvalidate")
-        pw.println("    lastMeasureCall=$lastMeasureCall")
-        pw.println("    lastDraw=$lastDraw")
-        pw.println("    lastTimeZoneChange=$lastTimeZoneChange")
         pw.println("    currText=$text")
         pw.println("    currTimeContextDesc=$contentDescription")
-        pw.println("    lastAnimationCall=$lastAnimationCall")
         pw.println("    dozingWeightInternal=$dozingWeightInternal")
         pw.println("    lockScreenWeightInternal=$lockScreenWeightInternal")
         pw.println("    dozingColor=$dozingColor")
@@ -580,6 +596,7 @@
             if (!clockView12Skel.contains("a")) {
                 sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
             }
+
             sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
             sCacheKey = key
         }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index e3c21cc..48821e8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -18,10 +18,8 @@
 import android.graphics.drawable.Drawable
 import android.net.Uri
 import android.os.Handler
-import android.os.UserHandle
 import android.provider.Settings
 import android.util.Log
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockId
@@ -31,7 +29,6 @@
 import com.android.systemui.plugins.PluginListener
 import com.android.systemui.shared.plugins.PluginManager
 import com.google.gson.Gson
-import javax.inject.Inject
 
 private val TAG = ClockRegistry::class.simpleName
 private const val DEBUG = true
@@ -41,22 +38,15 @@
     val context: Context,
     val pluginManager: PluginManager,
     val handler: Handler,
-    defaultClockProvider: ClockProvider
+    val isEnabled: Boolean,
+    userHandle: Int,
+    defaultClockProvider: ClockProvider,
 ) {
-    @Inject constructor(
-        context: Context,
-        pluginManager: PluginManager,
-        @Main handler: Handler,
-        defaultClockProvider: DefaultClockProvider
-    ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
-
     // Usually this would be a typealias, but a SAM provides better java interop
     fun interface ClockChangeListener {
         fun onClockChanged()
     }
 
-    var isEnabled: Boolean = false
-
     private val gson = Gson()
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
@@ -106,14 +96,19 @@
             )
         }
 
-        pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java,
-            true /* allowMultiple */)
-        context.contentResolver.registerContentObserver(
-            Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
-            false,
-            settingObserver,
-            UserHandle.USER_ALL
-        )
+        if (isEnabled) {
+            pluginManager.addPluginListener(
+                pluginListener,
+                ClockProviderPlugin::class.java,
+                /*allowMultiple=*/ true
+            )
+            context.contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+                /*notifyForDescendants=*/ false,
+                settingObserver,
+                userHandle
+            )
+        }
     }
 
     private fun connectClocks(provider: ClockProvider) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 6fd61da..da1d233 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Locale
@@ -86,9 +87,17 @@
         events.onTimeTick()
     }
 
+    override fun setLogBuffer(logBuffer: LogBuffer) {
+        smallClock.view.tag = "smallClockView"
+        largeClock.view.tag = "largeClockView"
+        smallClock.view.logBuffer = logBuffer
+        largeClock.view.logBuffer = logBuffer
+    }
+
     open inner class DefaultClockFaceController(
         override val view: AnimatableClockView,
     ) : ClockFaceController {
+
         // MAGENTA is a placeholder, and will be assigned correctly in initialize
         private var currentColor = Color.MAGENTA
         private var isRegionDark = false
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index 72f8b7b..40c8774 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -1,13 +1,16 @@
 package com.android.systemui.shared.recents.utilities;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.view.Surface;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.wm.shell.util.SplitBounds;
 
 /**
  * Utility class to position the thumbnail in the TaskView
@@ -16,10 +19,26 @@
 
     public static final float MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT = 0.1f;
 
+    /**
+     * Specifies that a stage is positioned at the top half of the screen if
+     * in portrait mode or at the left half of the screen if in landscape mode.
+     * TODO(b/254378592): Remove after consolidation
+     */
+    public static final int STAGE_POSITION_TOP_OR_LEFT = 0;
+
+    /**
+     * Specifies that a stage is positioned at the bottom half of the screen if
+     * in portrait mode or at the right half of the screen if in landscape mode.
+     * TODO(b/254378592): Remove after consolidation
+     */
+    public static final int STAGE_POSITION_BOTTOM_OR_RIGHT = 1;
+
     // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
     private final RectF mClippedInsets = new RectF();
     private final Matrix mMatrix = new Matrix();
     private boolean mIsOrientationChanged;
+    private SplitBounds mSplitBounds;
+    private int mDesiredStagePosition;
 
     public Matrix getMatrix() {
         return mMatrix;
@@ -33,6 +52,11 @@
         return mIsOrientationChanged;
     }
 
+    public void setSplitBounds(SplitBounds splitBounds, int desiredStagePosition) {
+        mSplitBounds = splitBounds;
+        mDesiredStagePosition = desiredStagePosition;
+    }
+
     /**
      * Updates the matrix based on the provided parameters
      */
@@ -42,10 +66,19 @@
         boolean isRotated = false;
         boolean isOrientationDifferent;
 
+        float fullscreenTaskWidth = screenWidthPx;
+        if (mSplitBounds != null && !mSplitBounds.appsStackedVertically) {
+            // For landscape, scale the width
+            float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
+                    ? mSplitBounds.leftTaskPercent
+                    : (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
+            // Scale landscape width to that of actual screen
+            fullscreenTaskWidth = screenWidthPx * taskPercent;
+        }
         int thumbnailRotation = thumbnailData.rotation;
         int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
         RectF thumbnailClipHint = new RectF();
-        float canvasScreenRatio = canvasWidth / (float) screenWidthPx;
+        float canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
         float scaledTaskbarSize = taskbarSize * canvasScreenRatio;
         thumbnailClipHint.bottom = isTablet ? scaledTaskbarSize : 0;
 
@@ -180,7 +213,7 @@
      * portrait or vice versa, {@code false} otherwise
      */
     private boolean isOrientationChange(int deltaRotation) {
-        return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+        return deltaRotation == ROTATION_90 || deltaRotation == ROTATION_270;
     }
 
     private void setThumbnailRotation(int deltaRotate, Rect thumbnailPosition) {
@@ -189,13 +222,13 @@
 
         mMatrix.setRotate(90 * deltaRotate);
         switch (deltaRotate) { /* Counter-clockwise */
-            case Surface.ROTATION_90:
+            case ROTATION_90:
                 translateX = thumbnailPosition.height();
                 break;
-            case Surface.ROTATION_270:
+            case ROTATION_270:
                 translateY = thumbnailPosition.width();
                 break;
-            case Surface.ROTATION_180:
+            case ROTATION_180:
                 translateX = thumbnailPosition.width();
                 translateY = thumbnailPosition.height();
                 break;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
index dd2e55d..cd4b999 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.shared.regionsampling
 
+import android.graphics.Color
 import android.graphics.Rect
 import android.view.View
 import androidx.annotation.VisibleForTesting
@@ -33,18 +34,19 @@
         regionSamplingEnabled: Boolean,
         updateFun: UpdateColorCallback
 ) {
-    private var isDark = RegionDarkness.DEFAULT
+    private var regionDarkness = RegionDarkness.DEFAULT
     private var samplingBounds = Rect()
     private val tmpScreenLocation = IntArray(2)
     @VisibleForTesting var regionSampler: RegionSamplingHelper? = null
-
+    private var lightForegroundColor = Color.WHITE
+    private var darkForegroundColor = Color.BLACK
     /**
      * Interface for method to be passed into RegionSamplingHelper
      */
     @FunctionalInterface
     interface UpdateColorCallback {
         /**
-         * Method to update the text colors after clock darkness changed.
+         * Method to update the foreground colors after clock darkness changed.
          */
         fun updateColors()
     }
@@ -59,6 +61,30 @@
         return RegionSamplingHelper(sampledView, callback, mainExecutor, bgExecutor)
     }
 
+    /**
+     * Sets the colors to be used for Dark and Light Foreground.
+     *
+     * @param lightColor The color used for Light Foreground.
+     * @param darkColor The color used for Dark Foreground.
+     */
+    fun setForegroundColors(lightColor: Int, darkColor: Int) {
+        lightForegroundColor = lightColor
+        darkForegroundColor = darkColor
+    }
+
+    /**
+     * Determines which foreground color to use based on region darkness.
+     *
+     * @return the determined foreground color
+     */
+    fun currentForegroundColor(): Int{
+        return if (regionDarkness.isDark) {
+            lightForegroundColor
+        } else {
+            darkForegroundColor
+        }
+    }
+
     private fun convertToClockDarkness(isRegionDark: Boolean): RegionDarkness {
         return if (isRegionDark) {
             RegionDarkness.DARK
@@ -68,7 +94,7 @@
     }
 
     fun currentRegionDarkness(): RegionDarkness {
-        return isDark
+        return regionDarkness
     }
 
     /**
@@ -97,7 +123,7 @@
             regionSampler = createRegionSamplingHelper(sampledView,
                     object : SamplingCallback {
                         override fun onRegionDarknessChanged(isRegionDark: Boolean) {
-                            isDark = convertToClockDarkness(isRegionDark)
+                            regionDarkness = convertToClockDarkness(isRegionDark)
                             updateFun.updateColors()
                         }
                         /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 8086172..42422d5 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -194,12 +194,8 @@
                             Rect homeContentInsets, Rect minimizedHomeBounds) {
                         final RecentsAnimationControllerCompat controllerCompat =
                                 new RecentsAnimationControllerCompat(controller);
-                        final RemoteAnimationTargetCompat[] appsCompat =
-                                RemoteAnimationTargetCompat.wrap(apps);
-                        final RemoteAnimationTargetCompat[] wallpapersCompat =
-                                RemoteAnimationTargetCompat.wrap(wallpapers);
-                        animationHandler.onAnimationStart(controllerCompat, appsCompat,
-                                wallpapersCompat, homeContentInsets, minimizedHomeBounds);
+                        animationHandler.onAnimationStart(controllerCompat, apps,
+                                wallpapers, homeContentInsets, minimizedHomeBounds);
                     }
 
                     @Override
@@ -210,12 +206,7 @@
 
                     @Override
                     public void onTasksAppeared(RemoteAnimationTarget[] apps) {
-                        final RemoteAnimationTargetCompat[] compats =
-                                new RemoteAnimationTargetCompat[apps.length];
-                        for (int i = 0; i < apps.length; ++i) {
-                            compats[i] = new RemoteAnimationTargetCompat(apps[i]);
-                        }
-                        animationHandler.onTasksAppeared(compats);
+                        animationHandler.onTasksAppeared(apps);
                     }
                 };
             }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index 5d6598d..8a25096 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -51,6 +51,8 @@
             InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER;
     public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION =
             InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
+    public static final int CUJ_RECENTS_SCROLLING =
+            InteractionJankMonitor.CUJ_RECENTS_SCROLLING;
 
     @IntDef({
             CUJ_APP_LAUNCH_FROM_RECENTS,
@@ -59,7 +61,8 @@
             CUJ_APP_CLOSE_TO_PIP,
             CUJ_QUICK_SWITCH,
             CUJ_APP_LAUNCH_FROM_WIDGET,
-            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            CUJ_RECENTS_SCROLLING
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index f2742b7..766266d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -110,6 +110,9 @@
     public static final int SYSUI_STATE_IMMERSIVE_MODE = 1 << 24;
     // The voice interaction session window is showing
     public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
+    // Freeform windows are showing in desktop mode
+    public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
+
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -137,7 +140,8 @@
             SYSUI_STATE_BACK_DISABLED,
             SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
             SYSUI_STATE_IMMERSIVE_MODE,
-            SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING
+            SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
+            SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE
     })
     public @interface SystemUiStateFlags {}
 
@@ -173,6 +177,8 @@
                 ? "bubbles_mange_menu_expanded" : "");
         str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : "");
         str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : "");
+        str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0
+                ? "freeform_active_in_desktop_mode" : "");
         return str.toString();
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 5cca4a6..8bddf21 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -17,6 +17,7 @@
 package com.android.systemui.shared.system;
 
 import android.graphics.Rect;
+import android.view.RemoteAnimationTarget;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -27,7 +28,7 @@
      * Called when the animation into Recents can start. This call is made on the binder thread.
      */
     void onAnimationStart(RecentsAnimationControllerCompat controller,
-            RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
             Rect homeContentInsets, Rect minimizedHomeBounds);
 
     /**
@@ -39,7 +40,7 @@
      * Called when the task of an activity that has been started while the recents animation
      * was running becomes ready for control.
      */
-    void onTasksAppeared(RemoteAnimationTargetCompat[] app);
+    void onTasksAppeared(RemoteAnimationTarget[] app);
 
     /**
      * Called to request that the current task tile be switched out for a screenshot (if not
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 09cf7c5..37e706a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -83,12 +83,6 @@
                     RemoteAnimationTarget[] wallpapers,
                     RemoteAnimationTarget[] nonApps,
                     final IRemoteAnimationFinishedCallback finishedCallback) {
-                final RemoteAnimationTargetCompat[] appsCompat =
-                        RemoteAnimationTargetCompat.wrap(apps);
-                final RemoteAnimationTargetCompat[] wallpapersCompat =
-                        RemoteAnimationTargetCompat.wrap(wallpapers);
-                final RemoteAnimationTargetCompat[] nonAppsCompat =
-                        RemoteAnimationTargetCompat.wrap(nonApps);
                 final Runnable animationFinishedCallback = new Runnable() {
                     @Override
                     public void run() {
@@ -100,8 +94,8 @@
                         }
                     }
                 };
-                remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat,
-                        nonAppsCompat, animationFinishedCallback);
+                remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers,
+                        nonApps, animationFinishedCallback);
             }
 
             @Override
@@ -121,12 +115,12 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
-                final RemoteAnimationTargetCompat[] appsCompat =
+                final RemoteAnimationTarget[] apps =
                         RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
-                final RemoteAnimationTargetCompat[] wallpapersCompat =
+                final RemoteAnimationTarget[] wallpapers =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, true /* wallpapers */, t, leashMap);
-                final RemoteAnimationTargetCompat[] nonAppsCompat =
+                final RemoteAnimationTarget[] nonApps =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, false /* wallpapers */, t, leashMap);
 
@@ -189,9 +183,9 @@
                         }
                     }
                     // Make wallpaper visible immediately since launcher apparently won't do this.
-                    for (int i = wallpapersCompat.length - 1; i >= 0; --i) {
-                        t.show(wallpapersCompat[i].leash);
-                        t.setAlpha(wallpapersCompat[i].leash, 1.f);
+                    for (int i = wallpapers.length - 1; i >= 0; --i) {
+                        t.show(wallpapers[i].leash);
+                        t.setAlpha(wallpapers[i].leash, 1.f);
                     }
                 } else {
                     if (launcherTask != null) {
@@ -237,7 +231,7 @@
                 }
                 // TODO(bc-unlcok): Pass correct transit type.
                 remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE,
-                        appsCompat, wallpapersCompat, nonAppsCompat, () -> {
+                        apps, wallpapers, nonApps, () -> {
                             synchronized (mFinishRunnables) {
                                 if (mFinishRunnables.remove(token) == null) return;
                             }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index 0076292..5809c81 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -16,11 +16,12 @@
 
 package com.android.systemui.shared.system;
 
+import android.view.RemoteAnimationTarget;
 import android.view.WindowManager;
 
 public interface RemoteAnimationRunnerCompat {
     void onAnimationStart(@WindowManager.TransitionOldType int transit,
-            RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
-            RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback);
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+            RemoteAnimationTarget[] nonApps, Runnable finishedCallback);
     void onAnimationCancelled();
 }
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 2d6bef5..8d1768c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -11,12 +11,15 @@
  * 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
+ * limitations under the License.
  */
 
 package com.android.systemui.shared.system;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -29,88 +32,28 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.TaskInfo;
 import android.app.WindowConfiguration;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.ArrayMap;
-import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
 
 import java.util.ArrayList;
+import java.util.function.BiPredicate;
 
 /**
- * @see RemoteAnimationTarget
+ * Some utility methods for creating {@link RemoteAnimationTarget} instances.
  */
 public class RemoteAnimationTargetCompat {
 
-    public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING;
-    public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING;
-    public static final int MODE_CHANGING = RemoteAnimationTarget.MODE_CHANGING;
-    public final int mode;
-
-    public static final int ACTIVITY_TYPE_UNDEFINED = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-    public static final int ACTIVITY_TYPE_STANDARD = WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-    public static final int ACTIVITY_TYPE_HOME = WindowConfiguration.ACTIVITY_TYPE_HOME;
-    public static final int ACTIVITY_TYPE_RECENTS = WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-    public static final int ACTIVITY_TYPE_ASSISTANT = WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-    public final int activityType;
-
-    public final int taskId;
-    public final SurfaceControl leash;
-    public final boolean isTranslucent;
-    public final Rect clipRect;
-    public final int prefixOrderIndex;
-    public final Point position;
-    public final Rect localBounds;
-    public final Rect sourceContainerBounds;
-    public final Rect screenSpaceBounds;
-    public final Rect startScreenSpaceBounds;
-    public final boolean isNotInRecents;
-    public final Rect contentInsets;
-    public final ActivityManager.RunningTaskInfo taskInfo;
-    public final boolean allowEnterPip;
-    public final int rotationChange;
-    public final int windowType;
-    public final WindowConfiguration windowConfiguration;
-
-    private final SurfaceControl mStartLeash;
-
-    // Fields used only to unwrap into RemoteAnimationTarget
-    private final Rect startBounds;
-
-    public final boolean willShowImeOnTarget;
-
-    public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
-        taskId = app.taskId;
-        mode = app.mode;
-        leash = app.leash;
-        isTranslucent = app.isTranslucent;
-        clipRect = app.clipRect;
-        position = app.position;
-        localBounds = app.localBounds;
-        sourceContainerBounds = app.sourceContainerBounds;
-        screenSpaceBounds = app.screenSpaceBounds;
-        startScreenSpaceBounds = screenSpaceBounds;
-        prefixOrderIndex = app.prefixOrderIndex;
-        isNotInRecents = app.isNotInRecents;
-        contentInsets = app.contentInsets;
-        activityType = app.windowConfiguration.getActivityType();
-        taskInfo = app.taskInfo;
-        allowEnterPip = app.allowEnterPip;
-        rotationChange = app.rotationChange;
-
-        mStartLeash = app.startLeash;
-        windowType = app.windowType;
-        windowConfiguration = app.windowConfiguration;
-        startBounds = app.startBounds;
-        willShowImeOnTarget = app.willShowImeOnTarget;
-    }
-
     private static int newModeToLegacyMode(int newMode) {
         switch (newMode) {
             case WindowManager.TRANSIT_OPEN:
@@ -120,21 +63,10 @@
             case WindowManager.TRANSIT_TO_BACK:
                 return MODE_CLOSING;
             default:
-                return 2; // MODE_CHANGING
+                return MODE_CHANGING;
         }
     }
 
-    public RemoteAnimationTarget unwrap() {
-        final RemoteAnimationTarget target = new RemoteAnimationTarget(
-                taskId, mode, leash, isTranslucent, clipRect, contentInsets,
-                prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
-                isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
-        );
-        target.setWillShowImeOnTarget(willShowImeOnTarget);
-        target.setRotationChange(rotationChange);
-        return target;
-    }
-
     /**
      * Almost a copy of Transitions#setupStartState.
      * TODO: remove when there is proper cross-process transaction sync.
@@ -206,54 +138,61 @@
         return leashSurface;
     }
 
-    public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order,
-            TransitionInfo info, SurfaceControl.Transaction t) {
-        mode = newModeToLegacyMode(change.getMode());
+    /**
+     * Creates a new RemoteAnimationTarget from the provided change info
+     */
+    public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+            TransitionInfo info, SurfaceControl.Transaction t,
+            @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+        int taskId;
+        boolean isNotInRecents;
+        ActivityManager.RunningTaskInfo taskInfo;
+        WindowConfiguration windowConfiguration;
+
         taskInfo = change.getTaskInfo();
         if (taskInfo != null) {
             taskId = taskInfo.taskId;
             isNotInRecents = !taskInfo.isRunning;
-            activityType = taskInfo.getActivityType();
             windowConfiguration = taskInfo.configuration.windowConfiguration;
         } else {
             taskId = INVALID_TASK_ID;
             isNotInRecents = true;
-            activityType = ACTIVITY_TYPE_UNDEFINED;
             windowConfiguration = new WindowConfiguration();
         }
 
-        // TODO: once we can properly sync transactions across process, then get rid of this leash.
-        leash = createLeash(info, change, order, t);
-
-        isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0;
-        clipRect = null;
-        position = null;
-        localBounds = new Rect(change.getEndAbsBounds());
+        Rect localBounds = new Rect(change.getEndAbsBounds());
         localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
-        sourceContainerBounds = null;
-        screenSpaceBounds = new Rect(change.getEndAbsBounds());
-        startScreenSpaceBounds = new Rect(change.getStartAbsBounds());
 
-        prefixOrderIndex = order;
-        // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
-        contentInsets = new Rect(0, 0, 0, 0);
-        allowEnterPip = change.getAllowEnterPip();
-        mStartLeash = null;
-        rotationChange = change.getEndRotation() - change.getStartRotation();
-        windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
-                ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE;
-
-        startBounds = change.getStartAbsBounds();
-        willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
-    }
-
-    public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
-        final int length = apps != null ? apps.length : 0;
-        final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length];
-        for (int i = 0; i < length; i++) {
-            appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]);
+        RemoteAnimationTarget target = new RemoteAnimationTarget(
+                taskId,
+                newModeToLegacyMode(change.getMode()),
+                // TODO: once we can properly sync transactions across process,
+                // then get rid of this leash.
+                createLeash(info, change, order, t),
+                (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
+                null,
+                // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+                new Rect(0, 0, 0, 0),
+                order,
+                null,
+                localBounds,
+                new Rect(change.getEndAbsBounds()),
+                windowConfiguration,
+                isNotInRecents,
+                null,
+                new Rect(change.getStartAbsBounds()),
+                taskInfo,
+                change.getAllowEnterPip(),
+                (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
+                        ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE
+        );
+        target.setWillShowImeOnTarget(
+                (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
+        target.setRotationChange(change.getEndRotation() - change.getStartRotation());
+        if (leashMap != null) {
+            leashMap.put(change.getLeash(), target.leash);
         }
-        return appsCompat;
+        return target;
     }
 
     /**
@@ -262,35 +201,20 @@
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info,
+    public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-        final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
-        for (int i = 0; i < info.getChanges().size(); i++) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getTaskInfo() == null) continue;
-
-            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+        SparseBooleanArray childTaskTargets = new SparseBooleanArray();
+        return wrap(info, t, leashMap, (change, taskInfo) -> {
             // Children always come before parent since changes are in top-to-bottom z-order.
-            if (taskInfo != null) {
-                if (childTaskTargets.contains(taskInfo.taskId)) {
-                    // has children, so not a leaf. Skip.
-                    continue;
-                }
-                if (taskInfo.hasParentTask()) {
-                    childTaskTargets.put(taskInfo.parentTaskId, change);
-                }
+            if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) {
+                // has children, so not a leaf. Skip.
+                return false;
             }
-
-            final RemoteAnimationTargetCompat targetCompat =
-                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
-            if (leashMap != null) {
-                leashMap.put(change.getLeash(), targetCompat.leash);
+            if (taskInfo.hasParentTask()) {
+                childTaskTargets.put(taskInfo.parentTaskId, true);
             }
-            out.add(targetCompat);
-        }
-
-        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+            return true;
+        });
     }
 
     /**
@@ -301,38 +225,22 @@
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers,
+    public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-
-        for (int i = 0; i < info.getChanges().size(); i++) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getTaskInfo() != null) continue;
-
-            final boolean changeIsWallpaper =
-                    (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0;
-            if (wallpapers != changeIsWallpaper) continue;
-
-            final RemoteAnimationTargetCompat targetCompat =
-                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
-            if (leashMap != null) {
-                leashMap.put(change.getLeash(), targetCompat.leash);
-            }
-            out.add(targetCompat);
-        }
-
-        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+        return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null)
+                && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0));
     }
 
-    /**
-     * @see SurfaceControl#release()
-     */
-    public void release() {
-        if (leash != null) {
-            leash.release();
+    private static RemoteAnimationTarget[] wrap(TransitionInfo info,
+            SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+            BiPredicate<Change, TaskInfo> filter) {
+        final ArrayList<RemoteAnimationTarget> out = new ArrayList<>();
+        for (int i = 0; i < info.getChanges().size(); i++) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (filter.test(change, change.getTaskInfo())) {
+                out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap));
+            }
         }
-        if (mStartLeash != null) {
-            mStartLeash.release();
-        }
+        return out.toArray(new RemoteAnimationTarget[out.size()]);
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index f679225..d6655a7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -17,7 +17,9 @@
 package com.android.systemui.shared.system;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
@@ -27,8 +29,7 @@
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
 
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -45,6 +46,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.IRecentsAnimationController;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
@@ -127,9 +129,9 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishedCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
-                final RemoteAnimationTargetCompat[] apps =
+                final RemoteAnimationTarget[] apps =
                         RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
-                final RemoteAnimationTargetCompat[] wallpapers =
+                final RemoteAnimationTarget[] wallpapers =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, true /* wallpapers */, t, leashMap);
                 // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
@@ -230,7 +232,7 @@
         private PictureInPictureSurfaceTransaction mPipTransaction = null;
         private IBinder mTransition = null;
         private boolean mKeyguardLocked = false;
-        private RemoteAnimationTargetCompat[] mAppearedTargets;
+        private RemoteAnimationTarget[] mAppearedTargets;
         private boolean mWillFinishToHome = false;
 
         void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
@@ -325,18 +327,15 @@
             final int layer = mInfo.getChanges().size() * 3;
             mOpeningLeashes = new ArrayList<>();
             mOpeningHome = cancelRecents;
-            final RemoteAnimationTargetCompat[] targets =
-                    new RemoteAnimationTargetCompat[openingTasks.size()];
+            final RemoteAnimationTarget[] targets =
+                    new RemoteAnimationTarget[openingTasks.size()];
             for (int i = 0; i < openingTasks.size(); ++i) {
                 final TransitionInfo.Change change = openingTasks.valueAt(i);
                 mOpeningLeashes.add(change.getLeash());
                 // We are receiving new opening tasks, so convert to onTasksAppeared.
-                final RemoteAnimationTargetCompat target = new RemoteAnimationTargetCompat(
-                        change, layer, info, t);
-                mLeashMap.put(mOpeningLeashes.get(i), target.leash);
-                t.reparent(target.leash, mInfo.getRootLeash());
-                t.setLayer(target.leash, layer);
-                targets[i] = target;
+                targets[i] = newTarget(change, layer, info, t, mLeashMap);
+                t.reparent(targets[i].leash, mInfo.getRootLeash());
+                t.setLayer(targets[i].leash, layer);
             }
             t.apply();
             mAppearedTargets = targets;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
deleted file mode 100644
index 30c062b..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java
+++ /dev/null
@@ -1,380 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.HardwareRenderer;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.Message;
-import android.os.Trace;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-import android.view.View;
-import android.view.ViewRootImpl;
-
-import java.util.function.Consumer;
-
-/**
- * Helper class to apply surface transactions in sync with RenderThread.
- *
- * NOTE: This is a modification of {@link android.view.SyncRtSurfaceTransactionApplier}, we can't 
- *       currently reference that class from the shared lib as it is hidden.
- */
-public class SyncRtSurfaceTransactionApplierCompat {
-
-    public static final int FLAG_ALL = 0xffffffff;
-    public static final int FLAG_ALPHA = 1;
-    public static final int FLAG_MATRIX = 1 << 1;
-    public static final int FLAG_WINDOW_CROP = 1 << 2;
-    public static final int FLAG_LAYER = 1 << 3;
-    public static final int FLAG_CORNER_RADIUS = 1 << 4;
-    public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
-    public static final int FLAG_VISIBILITY = 1 << 6;
-    public static final int FLAG_RELATIVE_LAYER = 1 << 7;
-    public static final int FLAG_SHADOW_RADIUS = 1 << 8;
-
-    private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
-
-    private final SurfaceControl mBarrierSurfaceControl;
-    private final ViewRootImpl mTargetViewRootImpl;
-    private final Handler mApplyHandler;
-
-    private int mSequenceNumber = 0;
-    private int mPendingSequenceNumber = 0;
-    private Runnable mAfterApplyCallback;
-
-    /**
-     * @param targetView The view in the surface that acts as synchronization anchor.
-     */
-    public SyncRtSurfaceTransactionApplierCompat(View targetView) {
-        mTargetViewRootImpl = targetView != null ? targetView.getViewRootImpl() : null;
-        mBarrierSurfaceControl = mTargetViewRootImpl != null
-            ? mTargetViewRootImpl.getSurfaceControl() : null;
-
-        mApplyHandler = new Handler(new Callback() {
-            @Override
-            public boolean handleMessage(Message msg) {
-                if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
-                    onApplyMessage(msg.arg1);
-                    return true;
-                }
-                return false;
-            }
-        });
-    }
-
-    private void onApplyMessage(int seqNo) {
-        mSequenceNumber = seqNo;
-        if (mSequenceNumber == mPendingSequenceNumber && mAfterApplyCallback != null) {
-            Runnable r = mAfterApplyCallback;
-            mAfterApplyCallback = null;
-            r.run();
-        }
-    }
-
-    /**
-     * Schedules applying surface parameters on the next frame.
-     *
-     * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into
-     *               this method to avoid synchronization issues.
-     */
-    public void scheduleApply(final SyncRtSurfaceTransactionApplierCompat.SurfaceParams... params) {
-        if (mTargetViewRootImpl == null || mTargetViewRootImpl.getView() == null) {
-            return;
-        }
-
-        mPendingSequenceNumber++;
-        final int toApplySeqNo = mPendingSequenceNumber;
-        mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() {
-            @Override
-            public void onFrameDraw(long frame) {
-                if (mBarrierSurfaceControl == null || !mBarrierSurfaceControl.isValid()) {
-                    Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                            .sendToTarget();
-                    return;
-                }
-                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Sync transaction frameNumber=" + frame);
-                Transaction t = new Transaction();
-                for (int i = params.length - 1; i >= 0; i--) {
-                    SyncRtSurfaceTransactionApplierCompat.SurfaceParams surfaceParams =
-                            params[i];
-                    surfaceParams.applyTo(t);
-                }
-                if (mTargetViewRootImpl != null) {
-                    mTargetViewRootImpl.mergeWithNextTransaction(t, frame);
-                } else {
-                    t.apply();
-                }
-                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
-                Message.obtain(mApplyHandler, MSG_UPDATE_SEQUENCE_NUMBER, toApplySeqNo, 0)
-                        .sendToTarget();
-            }
-        });
-
-        // Make sure a frame gets scheduled.
-        mTargetViewRootImpl.getView().invalidate();
-    }
-
-    /**
-     * Calls the runnable when any pending apply calls have completed
-     */
-    public void addAfterApplyCallback(final Runnable afterApplyCallback) {
-        if (mSequenceNumber == mPendingSequenceNumber) {
-            afterApplyCallback.run();
-        } else {
-            if (mAfterApplyCallback == null) {
-                mAfterApplyCallback = afterApplyCallback;
-            } else {
-                final Runnable oldCallback = mAfterApplyCallback;
-                mAfterApplyCallback = new Runnable() {
-                    @Override
-                    public void run() {
-                        afterApplyCallback.run();
-                        oldCallback.run();
-                    }
-                };
-            }
-        }
-    }
-
-    public static void applyParams(TransactionCompat t,
-            SyncRtSurfaceTransactionApplierCompat.SurfaceParams params) {
-        params.applyTo(t.mTransaction);
-    }
-
-    /**
-     * Creates an instance of SyncRtSurfaceTransactionApplier, deferring until the target view is
-     * attached if necessary.
-     */
-    public static void create(final View targetView,
-            final Consumer<SyncRtSurfaceTransactionApplierCompat> callback) {
-        if (targetView == null) {
-            // No target view, no applier
-            callback.accept(null);
-        } else if (targetView.getViewRootImpl() != null) {
-            // Already attached, we're good to go
-            callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-        } else {
-            // Haven't been attached before we can get the view root
-            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(View v) {
-                    targetView.removeOnAttachStateChangeListener(this);
-                    callback.accept(new SyncRtSurfaceTransactionApplierCompat(targetView));
-                }
-
-                @Override
-                public void onViewDetachedFromWindow(View v) {
-                    // Do nothing
-                }
-            });
-        }
-    }
-
-    public static class SurfaceParams {
-        public static class Builder {
-            final SurfaceControl surface;
-            int flags;
-            float alpha;
-            float cornerRadius;
-            int backgroundBlurRadius;
-            Matrix matrix;
-            Rect windowCrop;
-            int layer;
-            SurfaceControl relativeTo;
-            int relativeLayer;
-            boolean visible;
-            float shadowRadius;
-
-            /**
-             * @param surface The surface to modify.
-             */
-            public Builder(SurfaceControl surface) {
-                this.surface = surface;
-            }
-
-            /**
-             * @param alpha The alpha value to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withAlpha(float alpha) {
-                this.alpha = alpha;
-                flags |= FLAG_ALPHA;
-                return this;
-            }
-
-            /**
-             * @param matrix The matrix to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withMatrix(Matrix matrix) {
-                this.matrix = new Matrix(matrix);
-                flags |= FLAG_MATRIX;
-                return this;
-            }
-
-            /**
-             * @param windowCrop The window crop to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withWindowCrop(Rect windowCrop) {
-                this.windowCrop = new Rect(windowCrop);
-                flags |= FLAG_WINDOW_CROP;
-                return this;
-            }
-
-            /**
-             * @param layer The layer to assign the surface.
-             * @return this Builder
-             */
-            public Builder withLayer(int layer) {
-                this.layer = layer;
-                flags |= FLAG_LAYER;
-                return this;
-            }
-
-            /**
-             * @param relativeTo The surface that's set relative layer to.
-             * @param relativeLayer The relative layer.
-             * @return this Builder
-             */
-            public Builder withRelativeLayerTo(SurfaceControl relativeTo, int relativeLayer) {
-                this.relativeTo = relativeTo;
-                this.relativeLayer = relativeLayer;
-                flags |= FLAG_RELATIVE_LAYER;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for rounded corners to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withCornerRadius(float radius) {
-                this.cornerRadius = radius;
-                flags |= FLAG_CORNER_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for the shadows to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withShadowRadius(float radius) {
-                this.shadowRadius = radius;
-                flags |= FLAG_SHADOW_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param radius the Radius for blur to apply to the background surfaces.
-             * @return this Builder
-             */
-            public Builder withBackgroundBlur(int radius) {
-                this.backgroundBlurRadius = radius;
-                flags |= FLAG_BACKGROUND_BLUR_RADIUS;
-                return this;
-            }
-
-            /**
-             * @param visible The visibility to apply to the surface.
-             * @return this Builder
-             */
-            public Builder withVisibility(boolean visible) {
-                this.visible = visible;
-                flags |= FLAG_VISIBILITY;
-                return this;
-            }
-
-            /**
-             * @return a new SurfaceParams instance
-             */
-            public SurfaceParams build() {
-                return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
-                        relativeTo, relativeLayer, cornerRadius, backgroundBlurRadius, visible,
-                        shadowRadius);
-            }
-        }
-
-        private SurfaceParams(SurfaceControl surface, int flags, float alpha, Matrix matrix,
-                Rect windowCrop, int layer, SurfaceControl relativeTo, int relativeLayer,
-                float cornerRadius, int backgroundBlurRadius, boolean visible, float shadowRadius) {
-            this.flags = flags;
-            this.surface = surface;
-            this.alpha = alpha;
-            this.matrix = matrix;
-            this.windowCrop = windowCrop;
-            this.layer = layer;
-            this.relativeTo = relativeTo;
-            this.relativeLayer = relativeLayer;
-            this.cornerRadius = cornerRadius;
-            this.backgroundBlurRadius = backgroundBlurRadius;
-            this.visible = visible;
-            this.shadowRadius = shadowRadius;
-        }
-
-        private final int flags;
-        private final float[] mTmpValues = new float[9];
-
-        public final SurfaceControl surface;
-        public final float alpha;
-        public final float cornerRadius;
-        public final int backgroundBlurRadius;
-        public final Matrix matrix;
-        public final Rect windowCrop;
-        public final int layer;
-        public final SurfaceControl relativeTo;
-        public final int relativeLayer;
-        public final boolean visible;
-        public final float shadowRadius;
-
-        public void applyTo(SurfaceControl.Transaction t) {
-            if ((flags & FLAG_MATRIX) != 0) {
-                t.setMatrix(surface, matrix, mTmpValues);
-            }
-            if ((flags & FLAG_WINDOW_CROP) != 0) {
-                t.setWindowCrop(surface, windowCrop);
-            }
-            if ((flags & FLAG_ALPHA) != 0) {
-                t.setAlpha(surface, alpha);
-            }
-            if ((flags & FLAG_LAYER) != 0) {
-                t.setLayer(surface, layer);
-            }
-            if ((flags & FLAG_CORNER_RADIUS) != 0) {
-                t.setCornerRadius(surface, cornerRadius);
-            }
-            if ((flags & FLAG_BACKGROUND_BLUR_RADIUS) != 0) {
-                t.setBackgroundBlurRadius(surface, backgroundBlurRadius);
-            }
-            if ((flags & FLAG_VISIBILITY) != 0) {
-                if (visible) {
-                    t.show(surface);
-                } else {
-                    t.hide(surface);
-                }
-            }
-            if ((flags & FLAG_RELATIVE_LAYER) != 0) {
-                t.setRelativeLayer(surface, relativeTo, relativeLayer);
-            }
-            if ((flags & FLAG_SHADOW_RADIUS) != 0) {
-                t.setShadowRadius(surface, shadowRadius);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
deleted file mode 100644
index 43a882a5..0000000
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TransactionCompat.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.shared.system;
-
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.Transaction;
-
-public class TransactionCompat {
-
-    final Transaction mTransaction;
-
-    final float[] mTmpValues = new float[9];
-
-    public TransactionCompat() {
-        mTransaction = new Transaction();
-    }
-
-    public void apply() {
-        mTransaction.apply();
-    }
-
-    public TransactionCompat show(SurfaceControl surfaceControl) {
-        mTransaction.show(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat hide(SurfaceControl surfaceControl) {
-        mTransaction.hide(surfaceControl);
-        return this;
-    }
-
-    public TransactionCompat setPosition(SurfaceControl surfaceControl, float x, float y) {
-        mTransaction.setPosition(surfaceControl, x, y);
-        return this;
-    }
-
-    public TransactionCompat setSize(SurfaceControl surfaceControl, int w, int h) {
-        mTransaction.setBufferSize(surfaceControl, w, h);
-        return this;
-    }
-
-    public TransactionCompat setLayer(SurfaceControl surfaceControl, int z) {
-        mTransaction.setLayer(surfaceControl, z);
-        return this;
-    }
-
-    public TransactionCompat setAlpha(SurfaceControl surfaceControl, float alpha) {
-        mTransaction.setAlpha(surfaceControl, alpha);
-        return this;
-    }
-
-    public TransactionCompat setOpaque(SurfaceControl surfaceControl, boolean opaque) {
-        mTransaction.setOpaque(surfaceControl, opaque);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, float dsdx, float dtdx,
-            float dtdy, float dsdy) {
-        mTransaction.setMatrix(surfaceControl, dsdx, dtdx, dtdy, dsdy);
-        return this;
-    }
-
-    public TransactionCompat setMatrix(SurfaceControl surfaceControl, Matrix matrix) {
-        mTransaction.setMatrix(surfaceControl, matrix, mTmpValues);
-        return this;
-    }
-
-    public TransactionCompat setWindowCrop(SurfaceControl surfaceControl, Rect crop) {
-        mTransaction.setWindowCrop(surfaceControl, crop);
-        return this;
-    }
-
-    public TransactionCompat setCornerRadius(SurfaceControl surfaceControl, float radius) {
-        mTransaction.setCornerRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setBackgroundBlurRadius(SurfaceControl surfaceControl, int radius) {
-        mTransaction.setBackgroundBlurRadius(surfaceControl, radius);
-        return this;
-    }
-
-    public TransactionCompat setColor(SurfaceControl surfaceControl, float[] color) {
-        mTransaction.setColor(surfaceControl, color);
-        return this;
-    }
-
-    public static void setRelativeLayer(Transaction t, SurfaceControl surfaceControl,
-            SurfaceControl relativeTo, int z) {
-        t.setRelativeLayer(surfaceControl, relativeTo, z);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
index 5277e40..450784e 100644
--- a/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
+++ b/packages/SystemUI/src/com/android/keyguard/BouncerKeyguardMessageArea.kt
@@ -70,7 +70,7 @@
     }
 
     override fun setMessage(msg: CharSequence?) {
-        if (msg == textAboutToShow || msg == text) {
+        if ((msg == textAboutToShow && msg != null) || msg == text) {
             return
         }
         textAboutToShow = msg
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 9151238..386c095 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -23,12 +23,20 @@
 import android.text.format.DateFormat
 import android.util.TypedValue
 import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.REGION_SAMPLING
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.dagger.KeyguardClockLog
 import com.android.systemui.plugins.ClockController
-import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.regionsampling.RegionSamplingInstance
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -38,13 +46,20 @@
 import java.util.TimeZone
 import java.util.concurrent.Executor
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
 
 /**
  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
  * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
  */
 open class ClockEventController @Inject constructor(
-    private val statusBarStateController: StatusBarStateController,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val batteryController: BatteryController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@@ -53,12 +68,14 @@
     private val context: Context,
     @Main private val mainExecutor: Executor,
     @Background private val bgExecutor: Executor,
-    private val featureFlags: FeatureFlags,
+    @KeyguardClockLog private val logBuffer: LogBuffer,
+    private val featureFlags: FeatureFlags
 ) {
     var clock: ClockController? = null
         set(value) {
             field = value
             if (value != null) {
+                value.setLogBuffer(logBuffer)
                 value.initialize(resources, dozeAmount, 0f)
                 updateRegionSamplers(value)
             }
@@ -70,9 +87,9 @@
     private var isCharging = false
     private var dozeAmount = 0f
     private var isKeyguardVisible = false
-
-    private val regionSamplingEnabled =
-            featureFlags.isEnabled(com.android.systemui.flags.Flags.REGION_SAMPLING)
+    private var isRegistered = false
+    private var disposableHandle: DisposableHandle? = null
+    private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
 
     private fun updateColors() {
         if (regionSamplingEnabled && smallRegionSampler != null && largeRegionSampler != null) {
@@ -165,15 +182,6 @@
         }
     }
 
-    private val statusBarStateListener = object : StatusBarStateController.StateListener {
-        override fun onDozeAmountChanged(linear: Float, eased: Float) {
-            clock?.animations?.doze(linear)
-
-            isDozing = linear > dozeAmount
-            dozeAmount = linear
-        }
-    }
-
     private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
         override fun onKeyguardVisibilityChanged(visible: Boolean) {
             isKeyguardVisible = visible
@@ -195,13 +203,11 @@
         }
     }
 
-    init {
-        isDozing = statusBarStateController.isDozing
-    }
-
-    fun registerListeners() {
-        dozeAmount = statusBarStateController.dozeAmount
-        isDozing = statusBarStateController.isDozing || dozeAmount != 0f
+    fun registerListeners(parent: View) {
+        if (isRegistered) {
+            return
+        }
+        isRegistered = true
 
         broadcastDispatcher.registerReceiver(
             localeBroadcastReceiver,
@@ -210,17 +216,28 @@
         configurationController.addCallback(configListener)
         batteryController.addCallback(batteryCallback)
         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        statusBarStateController.addCallback(statusBarStateListener)
         smallRegionSampler?.startRegionSampler()
         largeRegionSampler?.startRegionSampler()
+        disposableHandle = parent.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                listenForDozing(this)
+                listenForDozeAmount(this)
+                listenForDozeAmountTransition(this)
+            }
+        }
     }
 
     fun unregisterListeners() {
+        if (!isRegistered) {
+            return
+        }
+        isRegistered = false
+
+        disposableHandle?.dispose()
         broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
         configurationController.removeCallback(configListener)
         batteryController.removeCallback(batteryCallback)
         keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
-        statusBarStateController.removeCallback(statusBarStateListener)
         smallRegionSampler?.stopRegionSampler()
         largeRegionSampler?.stopRegionSampler()
     }
@@ -235,8 +252,39 @@
         largeRegionSampler?.dump(pw)
     }
 
-    companion object {
-        private val TAG = ClockEventController::class.simpleName
-        private const val FORMAT_NUMBER = 1234567890
+    @VisibleForTesting
+    internal fun listenForDozeAmount(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardInteractor.dozeAmount.collect {
+                dozeAmount = it
+                clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardTransitionInteractor.aodToLockscreenTransition.collect {
+                // Would eventually run this:
+                // dozeAmount = it.value
+                // clock?.animations?.doze(dozeAmount)
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal fun listenForDozing(scope: CoroutineScope): Job {
+        return scope.launch {
+            combine (
+                keyguardInteractor.dozeAmount,
+                keyguardInteractor.isDozing,
+            ) { localDozeAmount, localIsDozing ->
+                localDozeAmount > dozeAmount || localIsDozing
+            }
+            .collect { localIsDozing ->
+                isDozing = localIsDozing
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index d03ef98..8ebad6c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -127,7 +127,7 @@
         if (useLargeClock) {
             out = mSmallClockFrame;
             in = mLargeClockFrame;
-            if (indexOfChild(in) == -1) addView(in);
+            if (indexOfChild(in) == -1) addView(in, 0);
             direction = -1;
             statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
                     + mSmartspaceTopOffset;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 20d064b..ace942d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -37,8 +37,6 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
@@ -120,8 +118,7 @@
             SecureSettings secureSettings,
             @Main Executor uiExecutor,
             DumpManager dumpManager,
-            ClockEventController clockEventController,
-            FeatureFlags featureFlags) {
+            ClockEventController clockEventController) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mClockRegistry = clockRegistry;
@@ -134,7 +131,6 @@
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
 
-        mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS));
         mClockChangedListener = () -> {
             setClock(mClockRegistry.createCurrentClock());
         };
@@ -165,7 +161,7 @@
     protected void onViewAttached() {
         mClockRegistry.registerClockChangeListener(mClockChangedListener);
         setClock(mClockRegistry.createCurrentClock());
-        mClockEventController.registerListeners();
+        mClockEventController.registerListeners(mView);
         mKeyguardClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index bcd1a1e..81305f9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -219,13 +219,16 @@
     };
 
 
-    private SwipeListener mSwipeListener = new SwipeListener() {
+    private final SwipeListener mSwipeListener = new SwipeListener() {
         @Override
         public void onSwipeUp() {
             if (!mUpdateMonitor.isFaceDetectionRunning()) {
-                mUpdateMonitor.requestFaceAuth(true, FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+                boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+                        FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
                 mKeyguardSecurityCallback.userActivity();
-                showMessage(null, null);
+                if (didFaceAuthRun) {
+                    showMessage(null, null);
+                }
             }
             if (mUpdateMonitor.isFaceEnrolled()) {
                 mUpdateMonitor.requestActiveUnlock(
@@ -234,7 +237,7 @@
             }
         }
     };
-    private ConfigurationController.ConfigurationListener mConfigurationListener =
+    private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
                 @Override
                 public void onThemeChanged() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index f558276..aff9dcb 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -106,7 +106,6 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -2352,11 +2351,13 @@
      * @param userInitiatedRequest true if the user explicitly requested face auth
      * @param reason One of the reasons {@link FaceAuthApiRequestReason} on why this API is being
      * invoked.
+     * @return current face auth detection state, true if it is running.
      */
-    public void requestFaceAuth(boolean userInitiatedRequest,
+    public boolean requestFaceAuth(boolean userInitiatedRequest,
             @FaceAuthApiRequestReason String reason) {
         mLogger.logFaceAuthRequested(userInitiatedRequest, reason);
         updateFaceListeningState(BIOMETRIC_ACTION_START, apiRequestReasonToUiEvent(reason));
+        return isFaceDetectionRunning();
     }
 
     /**
@@ -2366,10 +2367,6 @@
         stopListeningForFace(FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER);
     }
 
-    public boolean isFaceScanning() {
-        return mFaceRunningState == BIOMETRIC_STATE_RUNNING;
-    }
-
     private void updateFaceListeningState(int action, @NonNull FaceAuthUiEvent faceAuthUiEvent) {
         // If this message exists, we should not authenticate again until this message is
         // consumed by the handler
@@ -2417,7 +2414,7 @@
      * Attempts to trigger active unlock from trust agent.
      */
     private void requestActiveUnlock(
-            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
             String reason,
             boolean dismissKeyguard
     ) {
@@ -2447,7 +2444,7 @@
      * Only dismisses the keyguard under certain conditions.
      */
     public void requestActiveUnlock(
-            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
             String extraReason
     ) {
         final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
@@ -2714,7 +2711,7 @@
         return shouldListen;
     }
 
-    private void maybeLogListenerModelData(KeyguardListenModel model) {
+    private void maybeLogListenerModelData(@NonNull KeyguardListenModel model) {
         mLogger.logKeyguardListenerModel(model);
 
         if (model instanceof KeyguardActiveUnlockModel) {
@@ -3796,4 +3793,17 @@
         }
         mListenModels.print(pw);
     }
+
+    /**
+     * Schedules a watchdog for the face and fingerprint BiometricScheduler.
+     * Cancels all operations in the scheduler if it is hung for 10 seconds.
+     */
+    public void startBiometricWatchdog() {
+        if (mFaceManager != null) {
+            mFaceManager.scheduleWatchdog();
+        }
+        if (mFpm != null) {
+            mFpm.scheduleWatchdog();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
similarity index 86%
rename from packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
rename to packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
index c4be1ba535..72a44bd 100644
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockInfoModule.java
@@ -21,9 +21,14 @@
 import dagger.Module;
 import dagger.Provides;
 
-/** Dagger Module for clock package. */
+/**
+ * Dagger Module for clock package.
+ *
+ * @deprecated Migrate to ClockRegistry
+ */
 @Module
-public abstract class ClockModule {
+@Deprecated
+public abstract class ClockInfoModule {
 
     /** */
     @Provides
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
new file mode 100644
index 0000000..9767313
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard.dagger;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.UserHandle;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.shared.clocks.ClockRegistry;
+import com.android.systemui.shared.clocks.DefaultClockProvider;
+import com.android.systemui.shared.plugins.PluginManager;
+
+import dagger.Module;
+import dagger.Provides;
+
+/** Dagger Module for clocks. */
+@Module
+public abstract class ClockRegistryModule {
+    /** Provide the ClockRegistry as a singleton so that it is not instantiated more than once. */
+    @Provides
+    @SysUISingleton
+    public static ClockRegistry getClockRegistry(
+            @Application Context context,
+            PluginManager pluginManager,
+            @Main Handler handler,
+            DefaultClockProvider defaultClockProvider,
+            FeatureFlags featureFlags) {
+        return new ClockRegistry(
+                context,
+                pluginManager,
+                handler,
+                featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
+                UserHandle.USER_ALL,
+                defaultClockProvider);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 82b32cf..2f79e30 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -51,7 +51,7 @@
 
     fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg)
 
-    fun logActiveUnlockTriggered(reason: String) {
+    fun logActiveUnlockTriggered(reason: String?) {
         logBuffer.log("ActiveUnlock", DEBUG,
                 { str1 = reason },
                 { "initiate active unlock triggerReason=$str1" })
@@ -101,14 +101,14 @@
                 { "Face authenticated for wrong user: $int1" })
     }
 
-    fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String) {
+    fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String?) {
         logBuffer.log(TAG, DEBUG, {
                     int1 = msgId
                     str1 = helpMsg
                 }, { "Face help received, msgId: $int1 msg: $str1" })
     }
 
-    fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String) {
+    fun logFaceAuthRequested(userInitiatedRequest: Boolean, reason: String?) {
         logBuffer.log(TAG, DEBUG, {
             bool1 = userInitiatedRequest
             str1 = reason
@@ -187,7 +187,7 @@
                 { "No Profile Owner or Device Owner supervision app found for User $int1" })
     }
 
-    fun logPhoneStateChanged(newState: String) {
+    fun logPhoneStateChanged(newState: String?) {
         logBuffer.log(TAG, DEBUG,
                 { str1 = newState },
                 { "handlePhoneStateChanged($str1)" })
@@ -240,7 +240,7 @@
         }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" })
     }
 
-    fun logServiceStateIntent(action: String, serviceState: ServiceState?, subId: Int) {
+    fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) {
         logBuffer.log(TAG, VERBOSE, {
             str1 = action
             str2 = "$serviceState"
@@ -256,7 +256,7 @@
         }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" })
     }
 
-    fun logSimStateFromIntent(action: String, extraSimState: String, slotId: Int, subId: Int) {
+    fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) {
         logBuffer.log(TAG, VERBOSE, {
             str1 = action
             str2 = extraSimState
@@ -289,7 +289,7 @@
                 { "SubInfo:$str1" })
     }
 
-    fun logTimeFormatChanged(newTimeFormat: String) {
+    fun logTimeFormatChanged(newTimeFormat: String?) {
         logBuffer.log(TAG, DEBUG,
                 { str1 = newTimeFormat },
                 { "handleTimeFormatUpdate timeFormat=$str1" })
@@ -338,18 +338,18 @@
 
     fun logUserRequestedUnlock(
         requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN,
-        reason: String,
+        reason: String?,
         dismissKeyguard: Boolean
     ) {
         logBuffer.log("ActiveUnlock", DEBUG, {
-                    str1 = requestOrigin.name
+                    str1 = requestOrigin?.name
                     str2 = reason
                     bool1 = dismissKeyguard
                 }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" })
     }
 
     fun logShowTrustGrantedMessage(
-            message: String
+            message: String?
     ) {
         logBuffer.log(TAG, DEBUG, {
             str1 = message
diff --git a/packages/SystemUI/src/com/android/systemui/Dumpable.java b/packages/SystemUI/src/com/android/systemui/Dumpable.java
index 6525951..73fdce6 100644
--- a/packages/SystemUI/src/com/android/systemui/Dumpable.java
+++ b/packages/SystemUI/src/com/android/systemui/Dumpable.java
@@ -30,7 +30,6 @@
 
     /**
      * Called when it's time to dump the internal state
-     * @param fd A file descriptor.
      * @param pw Where to write your dump to.
      * @param args Arguments.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index c595586..3e0fa45 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -19,6 +19,7 @@
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
 import android.animation.AnimatorSet
+import android.animation.TimeInterpolator
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Canvas
@@ -55,7 +56,7 @@
     private val rimRect = RectF()
     private var cameraProtectionColor = Color.BLACK
     var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context,
-            com.android.systemui.R.attr.wallpaperTextColorAccent)
+            R.attr.wallpaperTextColorAccent)
     private var cameraProtectionAnimator: ValueAnimator? = null
     var hideOverlayRunnable: Runnable? = null
     var faceAuthSucceeded = false
@@ -84,46 +85,19 @@
     }
 
     override fun drawCutoutProtection(canvas: Canvas) {
-        if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) {
-            val rimPath = Path(protectionPath)
-            val scaleMatrix = Matrix().apply {
-                val rimBounds = RectF()
-                rimPath.computeBounds(rimBounds, true)
-                setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY())
-            }
-            rimPath.transform(scaleMatrix)
-            rimPaint.style = Paint.Style.FILL
-            val rimPaintAlpha = rimPaint.alpha
-            rimPaint.color = ColorUtils.blendARGB(
-                    faceScanningAnimColor,
-                    Color.WHITE,
-                    statusBarStateController.dozeAmount)
-            rimPaint.alpha = rimPaintAlpha
-            canvas.drawPath(rimPath, rimPaint)
+        if (protectionRect.isEmpty) {
+            return
         }
-
-        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE &&
-                !protectionRect.isEmpty) {
-            val scaledProtectionPath = Path(protectionPath)
-            val scaleMatrix = Matrix().apply {
-                val protectionPathRect = RectF()
-                scaledProtectionPath.computeBounds(protectionPathRect, true)
-                setScale(cameraProtectionProgress, cameraProtectionProgress,
-                        protectionPathRect.centerX(), protectionPathRect.centerY())
-            }
-            scaledProtectionPath.transform(scaleMatrix)
-            paint.style = Paint.Style.FILL
-            paint.color = cameraProtectionColor
-            canvas.drawPath(scaledProtectionPath, paint)
+        if (rimProgress > HIDDEN_RIM_SCALE) {
+            drawFaceScanningRim(canvas)
         }
-    }
-
-    override fun updateVisOnUpdateCutout(): Boolean {
-        return false // instead, we always update the visibility whenever face scanning starts/ends
+        if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE) {
+            drawCameraProtection(canvas)
+        }
     }
 
     override fun enableShowProtection(show: Boolean) {
-        val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show
+        val showScanningAnimNow = keyguardUpdateMonitor.isFaceDetectionRunning && show
         if (showScanningAnimNow == showScanningAnim) {
             return
         }
@@ -152,91 +126,26 @@
                     if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
                     else if (faceAuthSucceeded) Interpolators.STANDARD
                     else Interpolators.STANDARD_DECELERATE
-            addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                animation: ValueAnimator ->
-                cameraProtectionProgress = animation.animatedValue as Float
-                invalidate()
-            })
+            addUpdateListener(this@FaceScanningOverlay::updateCameraProtectionProgress)
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     cameraProtectionAnimator = null
                     if (!showScanningAnim) {
-                        visibility = View.INVISIBLE
-                        hideOverlayRunnable?.run()
-                        hideOverlayRunnable = null
-                        requestLayout()
+                        hide()
                     }
                 }
             })
         }
 
         rimAnimator?.cancel()
-        rimAnimator = AnimatorSet().apply {
-            if (showScanningAnim) {
-                val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE,
-                        PULSE_RADIUS_OUT).apply {
-                    duration = PULSE_APPEAR_DURATION
-                    interpolator = Interpolators.STANDARD_DECELERATE
-                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                        animation: ValueAnimator ->
-                        rimProgress = animation.animatedValue as Float
-                        invalidate()
-                    })
-                }
-
-                // animate in camera protection, rim, and then pulse in/out
-                playSequentially(cameraProtectionAnimator, rimAppearAnimator,
-                        createPulseAnimator(), createPulseAnimator(),
-                        createPulseAnimator(), createPulseAnimator(),
-                        createPulseAnimator(), createPulseAnimator())
-            } else {
-                val rimDisappearAnimator = ValueAnimator.ofFloat(
-                        rimProgress,
-                        if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS
-                        else SHOW_CAMERA_PROTECTION_SCALE
-                ).apply {
-                    duration =
-                            if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
-                            else PULSE_ERROR_DISAPPEAR_DURATION
-                    interpolator =
-                            if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE
-                            else Interpolators.STANDARD
-                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                        animation: ValueAnimator ->
-                        rimProgress = animation.animatedValue as Float
-                        invalidate()
-                    })
-                    addListener(object : AnimatorListenerAdapter() {
-                        override fun onAnimationEnd(animation: Animator) {
-                            rimProgress = HIDDEN_RIM_SCALE
-                            invalidate()
-                        }
-                    })
-                }
-                if (faceAuthSucceeded) {
-                    val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply {
-                        duration = PULSE_SUCCESS_DISAPPEAR_DURATION
-                        interpolator = Interpolators.LINEAR
-                        addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                            animation: ValueAnimator ->
-                            rimPaint.alpha = animation.animatedValue as Int
-                            invalidate()
-                        })
-                        addListener(object : AnimatorListenerAdapter() {
-                            override fun onAnimationEnd(animation: Animator) {
-                                rimPaint.alpha = 255
-                                invalidate()
-                            }
-                        })
-                    }
-                    val rimSuccessAnimator = AnimatorSet()
-                    rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator)
-                    playTogether(rimSuccessAnimator, cameraProtectionAnimator)
-                } else {
-                    playTogether(rimDisappearAnimator, cameraProtectionAnimator)
-                }
-            }
-
+        rimAnimator = if (showScanningAnim) {
+            createFaceScanningRimAnimator()
+        } else if (faceAuthSucceeded) {
+            createFaceSuccessRimAnimator()
+        } else {
+            createFaceNotSuccessRimAnimator()
+        }
+        rimAnimator?.apply {
             addListener(object : AnimatorListenerAdapter() {
                 override fun onAnimationEnd(animation: Animator) {
                     rimAnimator = null
@@ -245,34 +154,12 @@
                     }
                 }
             })
-            start()
         }
+        rimAnimator?.start()
     }
 
-    fun createPulseAnimator(): AnimatorSet {
-        return AnimatorSet().apply {
-            val pulseInwards = ValueAnimator.ofFloat(
-                    PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
-                duration = PULSE_DURATION_INWARDS
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
-            val pulseOutwards = ValueAnimator.ofFloat(
-                    PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply {
-                duration = PULSE_DURATION_OUTWARDS
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
-            playSequentially(pulseInwards, pulseOutwards)
-        }
+    override fun updateVisOnUpdateCutout(): Boolean {
+        return false // instead, we always update the visibility whenever face scanning starts/ends
     }
 
     override fun updateProtectionBoundingPath() {
@@ -290,17 +177,153 @@
             // Make sure that our measured height encompasses the extra space for the animation
             mTotalBounds.union(mBoundingRect)
             mTotalBounds.union(
-                    rimRect.left.toInt(),
-                    rimRect.top.toInt(),
-                    rimRect.right.toInt(),
-                    rimRect.bottom.toInt())
+                rimRect.left.toInt(),
+                rimRect.top.toInt(),
+                rimRect.right.toInt(),
+                rimRect.bottom.toInt())
             setMeasuredDimension(
-                    resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
-                    resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
+                resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
+                resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0))
         } else {
             setMeasuredDimension(
-                    resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
-                    resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
+                resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
+                resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0))
+        }
+    }
+
+    private fun drawFaceScanningRim(canvas: Canvas) {
+        val rimPath = Path(protectionPath)
+        scalePath(rimPath, rimProgress)
+        rimPaint.style = Paint.Style.FILL
+        val rimPaintAlpha = rimPaint.alpha
+        rimPaint.color = ColorUtils.blendARGB(
+            faceScanningAnimColor,
+            Color.WHITE,
+            statusBarStateController.dozeAmount
+        )
+        rimPaint.alpha = rimPaintAlpha
+        canvas.drawPath(rimPath, rimPaint)
+    }
+
+    private fun drawCameraProtection(canvas: Canvas) {
+        val scaledProtectionPath = Path(protectionPath)
+        scalePath(scaledProtectionPath, cameraProtectionProgress)
+        paint.style = Paint.Style.FILL
+        paint.color = cameraProtectionColor
+        canvas.drawPath(scaledProtectionPath, paint)
+    }
+
+    private fun createFaceSuccessRimAnimator(): AnimatorSet {
+        val rimSuccessAnimator = AnimatorSet()
+        rimSuccessAnimator.playTogether(
+            createRimDisappearAnimator(
+                PULSE_RADIUS_SUCCESS,
+                PULSE_SUCCESS_DISAPPEAR_DURATION,
+                Interpolators.STANDARD_DECELERATE
+            ),
+            createSuccessOpacityAnimator(),
+        )
+        return AnimatorSet().apply {
+            playTogether(rimSuccessAnimator, cameraProtectionAnimator)
+        }
+    }
+
+    private fun createFaceNotSuccessRimAnimator(): AnimatorSet {
+        return AnimatorSet().apply {
+            playTogether(
+                createRimDisappearAnimator(
+                    SHOW_CAMERA_PROTECTION_SCALE,
+                    PULSE_ERROR_DISAPPEAR_DURATION,
+                    Interpolators.STANDARD
+                ),
+                cameraProtectionAnimator,
+            )
+        }
+    }
+
+    private fun createRimDisappearAnimator(
+        endValue: Float,
+        animDuration: Long,
+        timeInterpolator: TimeInterpolator
+    ): ValueAnimator {
+        return ValueAnimator.ofFloat(rimProgress, endValue).apply {
+            duration = animDuration
+            interpolator = timeInterpolator
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    rimProgress = HIDDEN_RIM_SCALE
+                    invalidate()
+                }
+            })
+        }
+    }
+
+    private fun createSuccessOpacityAnimator(): ValueAnimator {
+        return ValueAnimator.ofInt(255, 0).apply {
+            duration = PULSE_SUCCESS_DISAPPEAR_DURATION
+            interpolator = Interpolators.LINEAR
+            addUpdateListener(this@FaceScanningOverlay::updateRimAlpha)
+            addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) {
+                    rimPaint.alpha = 255
+                    invalidate()
+                }
+            })
+        }
+    }
+
+    private fun createFaceScanningRimAnimator(): AnimatorSet {
+        return AnimatorSet().apply {
+            playSequentially(
+                cameraProtectionAnimator,
+                createRimAppearAnimator(),
+                createPulseAnimator()
+            )
+        }
+    }
+
+    private fun createRimAppearAnimator(): ValueAnimator {
+        return ValueAnimator.ofFloat(
+            SHOW_CAMERA_PROTECTION_SCALE,
+            PULSE_RADIUS_OUT
+        ).apply {
+            duration = PULSE_APPEAR_DURATION
+            interpolator = Interpolators.STANDARD_DECELERATE
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
+        }
+    }
+
+    private fun hide() {
+        visibility = INVISIBLE
+        hideOverlayRunnable?.run()
+        hideOverlayRunnable = null
+        requestLayout()
+    }
+
+    private fun updateRimProgress(animator: ValueAnimator) {
+        rimProgress = animator.animatedValue as Float
+        invalidate()
+    }
+
+    private fun updateCameraProtectionProgress(animator: ValueAnimator) {
+        cameraProtectionProgress = animator.animatedValue as Float
+        invalidate()
+    }
+
+    private fun updateRimAlpha(animator: ValueAnimator) {
+        rimPaint.alpha = animator.animatedValue as Int
+        invalidate()
+    }
+
+    private fun createPulseAnimator(): ValueAnimator {
+        return ValueAnimator.ofFloat(
+                PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply {
+            duration = HALF_PULSE_DURATION
+            interpolator = Interpolators.STANDARD
+            repeatCount = 11 // Pulse inwards and outwards, reversing direction, 6 times
+            repeatMode = ValueAnimator.REVERSE
+            addUpdateListener(this@FaceScanningOverlay::updateRimProgress)
         }
     }
 
@@ -363,13 +386,24 @@
         private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
         private const val PULSE_APPEAR_DURATION = 250L // without start delay
 
-        private const val PULSE_DURATION_INWARDS = 500L
-        private const val PULSE_DURATION_OUTWARDS = 500L
+        private const val HALF_PULSE_DURATION = 500L
 
         private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
         private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
 
         private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
         private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
+
+        private fun scalePath(path: Path, scalingFactor: Float) {
+            val scaleMatrix = Matrix().apply {
+                val boundingRectangle = RectF()
+                path.computeBounds(boundingRectangle, true)
+                setScale(
+                    scalingFactor, scalingFactor,
+                    boundingRectangle.centerX(), boundingRectangle.centerY()
+                )
+            }
+            path.transform(scaleMatrix)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt
new file mode 100644
index 0000000..4c3a7ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ProtoDumpable.kt
@@ -0,0 +1,7 @@
+package com.android.systemui
+
+import com.android.systemui.dump.nano.SystemUIProtoDump
+
+interface ProtoDumpable : Dumpable {
+    fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
index 7bcba3c..50e0399 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java
@@ -121,6 +121,6 @@
                     DumpHandler.PRIORITY_ARG_CRITICAL};
         }
 
-        mDumpHandler.dump(pw, massagedArgs);
+        mDumpHandler.dump(fd, pw, massagedArgs);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 9f1c9b4..0a2dc5b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -597,6 +597,7 @@
                 case INTENT_ACTION_DPAD_CENTER: {
                     Intent intent = new Intent(intentAction);
                     intent.setPackage(context.getPackageName());
+                    intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
                     return PendingIntent.getBroadcast(context, 0, intent,
                             PendingIntent.FLAG_IMMUTABLE);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
new file mode 100644
index 0000000..d6d03990
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import static android.util.MathUtils.constrain;
+
+import static java.util.Objects.requireNonNull;
+
+import android.animation.ValueAnimator;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FlingAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.HashMap;
+
+/**
+ * Controls the interaction animations of the {@link MenuView}. Also, it will use the relative
+ * coordinate based on the {@link MenuViewLayer} to compute the offset of the {@link MenuView}.
+ */
+class MenuAnimationController {
+    private static final String TAG = "MenuAnimationController";
+    private static final boolean DEBUG = false;
+    private static final float MIN_PERCENT = 0.0f;
+    private static final float MAX_PERCENT = 1.0f;
+    private static final float COMPLETELY_OPAQUE = 1.0f;
+    private static final float FLING_FRICTION_SCALAR = 1.9f;
+    private static final float DEFAULT_FRICTION = 4.2f;
+    private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
+    private static final float SPRING_STIFFNESS = 700f;
+    private static final float ESCAPE_VELOCITY = 750f;
+
+    private static final int FADE_OUT_DURATION_MS = 1000;
+    private static final int FADE_EFFECT_DURATION_MS = 3000;
+
+    private final MenuView mMenuView;
+    private final ValueAnimator mFadeOutAnimator;
+    private final Handler mHandler;
+    private boolean mIsMovedToEdge;
+    private boolean mIsFadeEffectEnabled;
+
+    // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
+    // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
+    private final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
+            new HashMap<>();
+
+    MenuAnimationController(MenuView menuView) {
+        mMenuView = menuView;
+
+        mHandler = createUiHandler();
+        mFadeOutAnimator = new ValueAnimator();
+        mFadeOutAnimator.setDuration(FADE_OUT_DURATION_MS);
+        mFadeOutAnimator.addUpdateListener(
+                (animation) -> menuView.setAlpha((float) animation.getAnimatedValue()));
+    }
+
+    void moveToPosition(PointF position) {
+        moveToPositionX(position.x);
+        moveToPositionY(position.y);
+    }
+
+    void moveToPositionX(float positionX) {
+        DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+    }
+
+    private void moveToPositionY(float positionY) {
+        DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+    }
+
+    void moveToPositionYIfNeeded(float positionY) {
+        // If the list view was out of screen bounds, it would allow users to nest scroll inside
+        // and avoid conflicting with outer scroll.
+        final RecyclerView listView = (RecyclerView) mMenuView.getChildAt(/* index= */ 0);
+        if (listView.getOverScrollMode() == View.OVER_SCROLL_NEVER) {
+            moveToPositionY(positionY);
+        }
+    }
+
+    void moveToTopLeftPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.top));
+    }
+
+    void moveToTopRightPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.top));
+    }
+
+    void moveToBottomLeftPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.left, draggableBounds.bottom));
+    }
+
+    void moveToBottomRightPosition() {
+        mIsMovedToEdge = false;
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        moveAndPersistPosition(new PointF(draggableBounds.right, draggableBounds.bottom));
+    }
+
+    void moveAndPersistPosition(PointF position) {
+        moveToPosition(position);
+        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
+        constrainPositionAndUpdate(position);
+    }
+
+    void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) {
+        final boolean shouldMenuFlingLeft = isOnLeftSide()
+                ? velocityX < ESCAPE_VELOCITY
+                : velocityX < -ESCAPE_VELOCITY;
+
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        final float finalPositionX = shouldMenuFlingLeft
+                ? draggableBounds.left : draggableBounds.right;
+
+        final float minimumVelocityToReachEdge =
+                (finalPositionX - x) * (FLING_FRICTION_SCALAR * DEFAULT_FRICTION);
+
+        final float startXVelocity = shouldMenuFlingLeft
+                ? Math.min(minimumVelocityToReachEdge, velocityX)
+                : Math.max(minimumVelocityToReachEdge, velocityX);
+
+        flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
+                startXVelocity,
+                FLING_FRICTION_SCALAR,
+                new SpringForce()
+                        .setStiffness(SPRING_STIFFNESS)
+                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                finalPositionX);
+
+        flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
+                velocityY,
+                FLING_FRICTION_SCALAR,
+                new SpringForce()
+                        .setStiffness(SPRING_STIFFNESS)
+                        .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+                /* finalPosition= */ null);
+    }
+
+    private void flingThenSpringMenuWith(DynamicAnimation.ViewProperty property, float velocity,
+            float friction, SpringForce spring, Float finalPosition) {
+
+        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
+        final float currentValue = menuPositionProperty.getValue(mMenuView);
+        final Rect bounds = mMenuView.getMenuDraggableBounds();
+        final float min =
+                property.equals(DynamicAnimation.TRANSLATION_X)
+                        ? bounds.left
+                        : bounds.top;
+        final float max =
+                property.equals(DynamicAnimation.TRANSLATION_X)
+                        ? bounds.right
+                        : bounds.bottom;
+
+        final FlingAnimation flingAnimation = new FlingAnimation(mMenuView, menuPositionProperty);
+        flingAnimation.setFriction(friction)
+                .setStartVelocity(velocity)
+                .setMinValue(Math.min(currentValue, min))
+                .setMaxValue(Math.max(currentValue, max))
+                .addEndListener((animation, canceled, endValue, endVelocity) -> {
+                    if (canceled) {
+                        if (DEBUG) {
+                            Log.d(TAG, "The fling animation was canceled.");
+                        }
+
+                        return;
+                    }
+
+                    final float endPosition = finalPosition != null
+                            ? finalPosition
+                            : Math.max(min, Math.min(max, endValue));
+                    springMenuWith(property, spring, endVelocity, endPosition);
+                });
+
+        cancelAnimation(property);
+        mPositionAnimations.put(property, flingAnimation);
+        flingAnimation.start();
+    }
+
+    private void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
+            float velocity, float finalPosition) {
+        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
+        final SpringAnimation springAnimation =
+                new SpringAnimation(mMenuView, menuPositionProperty)
+                        .setSpring(spring)
+                        .addEndListener((animation, canceled, endValue, endVelocity) -> {
+                            if (canceled || endValue != finalPosition) {
+                                return;
+                            }
+
+                            onSpringAnimationEnd(new PointF(mMenuView.getTranslationX(),
+                                    mMenuView.getTranslationY()));
+                        })
+                        .setStartVelocity(velocity);
+
+        cancelAnimation(property);
+        mPositionAnimations.put(property, springAnimation);
+        springAnimation.animateToFinalPosition(finalPosition);
+    }
+
+    /**
+     * Determines whether to hide the menu to the edge of the screen with the given current
+     * translation x of the menu view. It should be used when receiving the action up touch event.
+     *
+     * @param currentXTranslation the current translation x of the menu view.
+     * @return true if the menu would be hidden to the edge, otherwise false.
+     */
+    boolean maybeMoveToEdgeAndHide(float currentXTranslation) {
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+
+        // If the translation x is zero, it should be at the left of the bound.
+        if (currentXTranslation < draggableBounds.left
+                || currentXTranslation > draggableBounds.right) {
+            moveToEdgeAndHide();
+            return true;
+        }
+
+        fadeOutIfEnabled();
+        return false;
+    }
+
+    private boolean isOnLeftSide() {
+        return mMenuView.getTranslationX() < mMenuView.getMenuDraggableBounds().centerX();
+    }
+
+    boolean isMovedToEdge() {
+        return mIsMovedToEdge;
+    }
+
+    void moveToEdgeAndHide() {
+        mIsMovedToEdge = true;
+
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        final float endY = constrain(mMenuView.getTranslationY(), draggableBounds.top,
+                draggableBounds.bottom);
+        final float menuHalfWidth = mMenuView.getWidth() / 2.0f;
+        final float endX = isOnLeftSide()
+                ? draggableBounds.left - menuHalfWidth
+                : draggableBounds.right + menuHalfWidth;
+        moveAndPersistPosition(new PointF(endX, endY));
+
+        // Keep the touch region let users could click extra space to pop up the menu view
+        // from the screen edge
+        mMenuView.onBoundsInParentChanged(isOnLeftSide()
+                ? draggableBounds.left
+                : draggableBounds.right, (int) mMenuView.getTranslationY());
+
+        fadeOutIfEnabled();
+    }
+
+    void moveOutEdgeAndShow() {
+        mIsMovedToEdge = false;
+
+        mMenuView.onPositionChanged();
+        mMenuView.onEdgeChangedIfNeeded();
+    }
+
+    void cancelAnimations() {
+        cancelAnimation(DynamicAnimation.TRANSLATION_X);
+        cancelAnimation(DynamicAnimation.TRANSLATION_Y);
+    }
+
+    private void cancelAnimation(DynamicAnimation.ViewProperty property) {
+        if (!mPositionAnimations.containsKey(property)) {
+            return;
+        }
+
+        mPositionAnimations.get(property).cancel();
+    }
+
+    void onDraggingStart() {
+        mMenuView.onDraggingStart();
+    }
+
+    private void onSpringAnimationEnd(PointF position) {
+        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
+        constrainPositionAndUpdate(position);
+
+        fadeOutIfEnabled();
+    }
+
+    private void constrainPositionAndUpdate(PointF position) {
+        final Rect draggableBounds = mMenuView.getMenuDraggableBounds();
+        // Have the space gap margin between the top bound and the menu view, so actually the
+        // position y range needs to cut the margin.
+        position.offset(-draggableBounds.left, -draggableBounds.top);
+
+        final float percentageX = position.x < draggableBounds.centerX()
+                ? MIN_PERCENT : MAX_PERCENT;
+
+        final float percentageY = position.y < 0 || draggableBounds.height() == 0
+                ? MIN_PERCENT
+                : Math.min(MAX_PERCENT, position.y / draggableBounds.height());
+        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+    }
+
+    void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
+        mIsFadeEffectEnabled = isFadeEffectEnabled;
+
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        mFadeOutAnimator.cancel();
+        mFadeOutAnimator.setFloatValues(COMPLETELY_OPAQUE, newOpacityValue);
+        mHandler.post(() -> mMenuView.setAlpha(
+                mIsFadeEffectEnabled ? newOpacityValue : COMPLETELY_OPAQUE));
+    }
+
+    void fadeInNowIfEnabled() {
+        if (!mIsFadeEffectEnabled) {
+            return;
+        }
+
+        cancelAndRemoveCallbacksAndMessages();
+        mHandler.post(() -> mMenuView.setAlpha(COMPLETELY_OPAQUE));
+    }
+
+    void fadeOutIfEnabled() {
+        if (!mIsFadeEffectEnabled) {
+            return;
+        }
+
+        cancelAndRemoveCallbacksAndMessages();
+        mHandler.postDelayed(mFadeOutAnimator::start, FADE_EFFECT_DURATION_MS);
+    }
+
+    private void cancelAndRemoveCallbacksAndMessages() {
+        mFadeOutAnimator.cancel();
+        mHandler.removeCallbacksAndMessages(/* token= */ null);
+    }
+
+    private Handler createUiHandler() {
+        return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
+    }
+
+    static class MenuPositionProperty
+            extends FloatPropertyCompat<MenuView> {
+        private final DynamicAnimation.ViewProperty mProperty;
+
+        MenuPositionProperty(DynamicAnimation.ViewProperty property) {
+            super(property.toString());
+            mProperty = property;
+        }
+
+        @Override
+        public float getValue(MenuView menuView) {
+            return mProperty.getValue(menuView);
+        }
+
+        @Override
+        public void setValue(MenuView menuView, float value) {
+            mProperty.setValue(menuView, value);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
new file mode 100644
index 0000000..83c344c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuFadeEffectInfo.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu
+
+import android.annotation.FloatRange
+
+@FloatRange(from = 0.0, to = 1.0) const val DEFAULT_OPACITY_VALUE = 0.55f
+const val DEFAULT_FADE_EFFECT_IS_ENABLED = 1
+
+/** The data class for the fade effect info of the accessibility floating menu view. */
+data class MenuFadeEffectInfo(val isFadeEffectEnabled: Boolean, val opacity: Float)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
index 698d60a..57019de 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java
@@ -16,22 +16,29 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED;
+import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_OPACITY;
 import static android.provider.Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE;
 import static android.provider.Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
 
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
+import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_FADE_EFFECT_IS_ENABLED;
+import static com.android.systemui.accessibility.floatingmenu.MenuFadeEffectInfoKt.DEFAULT_OPACITY_VALUE;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
 
+import android.annotation.FloatRange;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Prefs;
 
 import java.util.List;
 
@@ -39,9 +46,16 @@
  * Stores and observe the settings contents for the menu view.
  */
 class MenuInfoRepository {
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f;
+
+    @FloatRange(from = 0.0, to = 1.0)
+    private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.9f;
+
     private final Context mContext;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final OnSettingsContentsChanged mSettingsContentsCallback;
+    private Position mPercentagePosition;
 
     private final ContentObserver mMenuTargetFeaturesContentObserver =
             new ContentObserver(mHandler) {
@@ -62,9 +76,24 @@
                 }
             };
 
+    @VisibleForTesting
+    final ContentObserver mMenuFadeOutContentObserver =
+            new ContentObserver(mHandler) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mSettingsContentsCallback.onFadeEffectInfoChanged(getMenuFadeEffectInfo());
+                }
+            };
+
     MenuInfoRepository(Context context, OnSettingsContentsChanged settingsContentsChanged) {
         mContext = context;
         mSettingsContentsCallback = settingsContentsChanged;
+
+        mPercentagePosition = getStartPosition();
+    }
+
+    void loadMenuPosition(OnInfoReady<Position> callback) {
+        callback.onReady(mPercentagePosition);
     }
 
     void loadMenuTargetFeatures(OnInfoReady<List<AccessibilityTarget>> callback) {
@@ -75,6 +104,30 @@
         callback.onReady(getMenuSizeTypeFromSettings(mContext));
     }
 
+    void loadMenuFadeEffectInfo(OnInfoReady<MenuFadeEffectInfo> callback) {
+        callback.onReady(getMenuFadeEffectInfo());
+    }
+
+    private MenuFadeEffectInfo getMenuFadeEffectInfo() {
+        return new MenuFadeEffectInfo(isMenuFadeEffectEnabledFromSettings(mContext),
+                getMenuOpacityFromSettings(mContext));
+    }
+
+    void updateMenuSavingPosition(Position percentagePosition) {
+        mPercentagePosition = percentagePosition;
+        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION,
+                percentagePosition.toString());
+    }
+
+    private Position getStartPosition() {
+        final String absolutePositionString = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+
+        return TextUtils.isEmpty(absolutePositionString)
+                ? new Position(DEFAULT_MENU_POSITION_X_PERCENT, DEFAULT_MENU_POSITION_Y_PERCENT)
+                : Position.fromString(absolutePositionString);
+    }
+
     void registerContentObservers() {
         mContext.getContentResolver().registerContentObserver(
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS),
@@ -88,17 +141,28 @@
                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_FLOATING_MENU_SIZE),
                 /* notifyForDescendants */ false, mMenuSizeContentObserver,
                 UserHandle.USER_CURRENT);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED),
+                /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
+                UserHandle.USER_CURRENT);
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY),
+                /* notifyForDescendants */ false, mMenuFadeOutContentObserver,
+                UserHandle.USER_CURRENT);
     }
 
     void unregisterContentObservers() {
         mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);
         mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);
+        mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);
     }
 
     interface OnSettingsContentsChanged {
         void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures);
 
         void onSizeTypeChanged(int newSizeType);
+
+        void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo);
     }
 
     interface OnInfoReady<T> {
@@ -109,4 +173,16 @@
         return Settings.Secure.getIntForUser(context.getContentResolver(),
                 ACCESSIBILITY_FLOATING_MENU_SIZE, SMALL, UserHandle.USER_CURRENT);
     }
+
+    private static boolean isMenuFadeEffectEnabledFromSettings(Context context) {
+        return Settings.Secure.getIntForUser(context.getContentResolver(),
+                ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
+                DEFAULT_FADE_EFFECT_IS_ENABLED, UserHandle.USER_CURRENT) == /* enabled */ 1;
+    }
+
+    private static float getMenuOpacityFromSettings(Context context) {
+        return Settings.Secure.getFloatForUser(context.getContentResolver(),
+                ACCESSIBILITY_FLOATING_MENU_OPACITY, DEFAULT_OPACITY_VALUE,
+                UserHandle.USER_CURRENT);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
new file mode 100644
index 0000000..e69a248
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+
+import com.android.systemui.R;
+
+/**
+ * An accessibility item delegate for the individual items of the list view in the
+ * {@link MenuView}.
+ */
+class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate {
+    private final MenuAnimationController mAnimationController;
+
+    MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate,
+            MenuAnimationController animationController) {
+        super(recyclerViewDelegate);
+        mAnimationController = animationController;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+        super.onInitializeAccessibilityNodeInfo(host, info);
+
+        final Resources res = host.getResources();
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopLeft =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.action_move_top_left,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_top_left));
+        info.addAction(moveTopLeft);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveTopRight =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_top_right,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_top_right));
+        info.addAction(moveTopRight);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomLeft =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_bottom_left,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_bottom_left));
+        info.addAction(moveBottomLeft);
+
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveBottomRight =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                        R.id.action_move_bottom_right,
+                        res.getString(
+                                R.string.accessibility_floating_button_action_move_bottom_right));
+        info.addAction(moveBottomRight);
+
+        final int moveEdgeId = mAnimationController.isMovedToEdge()
+                ? R.id.action_move_out_edge_and_show
+                : R.id.action_move_to_edge_and_hide;
+        final int moveEdgeTextResId = mAnimationController.isMovedToEdge()
+                ? R.string.accessibility_floating_button_action_move_out_edge_and_show
+                : R.string.accessibility_floating_button_action_move_to_edge_and_hide_to_half;
+        final AccessibilityNodeInfoCompat.AccessibilityActionCompat moveToOrOutEdge =
+                new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId,
+                        res.getString(moveEdgeTextResId));
+        info.addAction(moveToOrOutEdge);
+    }
+
+    @Override
+    public boolean performAccessibilityAction(View host, int action, Bundle args) {
+        if (action == ACTION_ACCESSIBILITY_FOCUS) {
+            mAnimationController.fadeInNowIfEnabled();
+        }
+
+        if (action == ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
+            mAnimationController.fadeOutIfEnabled();
+        }
+
+        if (action == R.id.action_move_top_left) {
+            mAnimationController.moveToTopLeftPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_top_right) {
+            mAnimationController.moveToTopRightPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_bottom_left) {
+            mAnimationController.moveToBottomLeftPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_bottom_right) {
+            mAnimationController.moveToBottomRightPosition();
+            return true;
+        }
+
+        if (action == R.id.action_move_to_edge_and_hide) {
+            mAnimationController.moveToEdgeAndHide();
+            return true;
+        }
+
+        if (action == R.id.action_move_out_edge_and_show) {
+            mAnimationController.moveOutEdgeAndShow();
+            return true;
+        }
+
+        return super.performAccessibilityAction(host, action, args);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
new file mode 100644
index 0000000..3146c9f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * Controls the all touch events of the accessibility target features view{@link RecyclerView} in
+ * the {@link MenuView}. And then compute the gestures' velocity for fling and spring
+ * animations.
+ */
+class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener {
+    private static final int VELOCITY_UNIT_SECONDS = 1000;
+    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private final MenuAnimationController mMenuAnimationController;
+    private final PointF mDown = new PointF();
+    private final PointF mMenuTranslationDown = new PointF();
+    private boolean mIsDragging = false;
+    private float mTouchSlop;
+
+    MenuListViewTouchHandler(MenuAnimationController menuAnimationController) {
+        mMenuAnimationController = menuAnimationController;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
+            @NonNull MotionEvent motionEvent) {
+
+        final View menuView = (View) recyclerView.getParent();
+        addMovement(motionEvent);
+
+        final float dx = motionEvent.getRawX() - mDown.x;
+        final float dy = motionEvent.getRawY() - mDown.y;
+
+        switch (motionEvent.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mMenuAnimationController.fadeInNowIfEnabled();
+                mTouchSlop = ViewConfiguration.get(recyclerView.getContext()).getScaledTouchSlop();
+                mDown.set(motionEvent.getRawX(), motionEvent.getRawY());
+                mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY());
+
+                mMenuAnimationController.cancelAnimations();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) {
+                    if (!mIsDragging) {
+                        mIsDragging = true;
+                        mMenuAnimationController.onDraggingStart();
+                    }
+
+                    mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
+                    mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsDragging) {
+                    final float endX = mMenuTranslationDown.x + dx;
+                    mIsDragging = false;
+
+                    if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
+                        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS);
+                        mMenuAnimationController.flingMenuThenSpringToEdge(endX,
+                                mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+                    }
+
+                    // Avoid triggering the listener of the item.
+                    return true;
+                }
+
+                break;
+            default: // Do nothing
+        }
+
+        // not consume all the events here because keeping the scroll behavior of list view.
+        return false;
+    }
+
+    @Override
+    public void onTouchEvent(@NonNull RecyclerView recyclerView,
+            @NonNull MotionEvent motionEvent) {
+        // Do nothing
+    }
+
+    @Override
+    public void onRequestDisallowInterceptTouchEvent(boolean b) {
+        // Do nothing
+    }
+
+    /**
+     * Adds a movement to the velocity tracker using raw screen coordinates.
+     */
+    private void addMovement(MotionEvent motionEvent) {
+        final float deltaX = motionEvent.getRawX() - motionEvent.getX();
+        final float deltaY = motionEvent.getRawY() - motionEvent.getY();
+        motionEvent.offsetLocation(deltaX, deltaY);
+        mVelocityTracker.addMovement(motionEvent);
+        motionEvent.offsetLocation(-deltaX, -deltaY);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index 576f23e..15d139c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -21,28 +21,44 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.drawable.GradientDrawable;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.lifecycle.Observer;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
- * The container view displays the accessibility features.
+ * The menu view displays the accessibility features.
  */
 @SuppressLint("ViewConstructor")
-class MenuView extends FrameLayout {
+class MenuView extends FrameLayout implements
+        ViewTreeObserver.OnComputeInternalInsetsListener {
     private static final int INDEX_MENU_ITEM = 0;
     private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
     private final AccessibilityTargetAdapter mAdapter;
     private final MenuViewModel mMenuViewModel;
+    private final MenuAnimationController mMenuAnimationController;
+    private final Rect mBoundsInParent = new Rect();
     private final RecyclerView mTargetFeaturesView;
+    private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
+            this::updateSystemGestureExcludeRects;
+    private final Observer<MenuFadeEffectInfo> mFadeEffectInfoObserver =
+            this::onMenuFadeEffectInfoChanged;
+    private final Observer<Position> mPercentagePositionObserver = this::onPercentagePosition;
     private final Observer<Integer> mSizeTypeObserver = this::onSizeTypeChanged;
     private final Observer<List<AccessibilityTarget>> mTargetFeaturesObserver =
             this::onTargetFeaturesChanged;
@@ -53,23 +69,47 @@
 
         mMenuViewModel = menuViewModel;
         mMenuViewAppearance = menuViewAppearance;
+        mMenuAnimationController = new MenuAnimationController(this);
+
         mAdapter = new AccessibilityTargetAdapter(mTargetFeatures);
         mTargetFeaturesView = new RecyclerView(context);
         mTargetFeaturesView.setAdapter(mAdapter);
         mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context));
+        mTargetFeaturesView.setAccessibilityDelegateCompat(
+                new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) {
+                    @NonNull
+                    @Override
+                    public AccessibilityDelegateCompat getItemDelegate() {
+                        return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this,
+                                mMenuAnimationController);
+                    }
+                });
         setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
         // Avoid drawing out of bounds of the parent view
         setClipToOutline(true);
+
         loadLayoutResources();
 
         addView(mTargetFeaturesView);
     }
 
     @Override
+    public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+        inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+        inoutInfo.touchableRegion.set(mBoundsInParent);
+    }
+
+    @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
 
         loadLayoutResources();
+
+        mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
+    }
+
+    void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
+        mTargetFeaturesView.addOnItemTouchListener(listener);
     }
 
     @SuppressLint("NotifyDataSetChanged")
@@ -80,11 +120,25 @@
     }
 
     private void onSizeChanged() {
+        mBoundsInParent.set(mBoundsInParent.left, mBoundsInParent.top,
+                mBoundsInParent.left + mMenuViewAppearance.getMenuWidth(),
+                mBoundsInParent.top + mMenuViewAppearance.getMenuHeight());
+
         final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
         layoutParams.height = mMenuViewAppearance.getMenuHeight();
         setLayoutParams(layoutParams);
     }
 
+    void onEdgeChangedIfNeeded() {
+        final Rect draggableBounds = mMenuViewAppearance.getMenuDraggableBounds();
+        if (getTranslationX() != draggableBounds.left
+                && getTranslationX() != draggableBounds.right) {
+            return;
+        }
+
+        onEdgeChanged();
+    }
+
     private void onEdgeChanged() {
         final int[] insets = mMenuViewAppearance.getMenuInsets();
         getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
@@ -96,8 +150,22 @@
                 mMenuViewAppearance.getMenuStrokeColor());
     }
 
+    private void onPercentagePosition(Position percentagePosition) {
+        mMenuViewAppearance.setPercentagePosition(percentagePosition);
+
+        onPositionChanged();
+    }
+
+    void onPositionChanged() {
+        final PointF position = mMenuViewAppearance.getMenuPosition();
+        mMenuAnimationController.moveToPosition(position);
+        onBoundsInParentChanged((int) position.x, (int) position.y);
+    }
+
     @SuppressLint("NotifyDataSetChanged")
     private void onSizeTypeChanged(int newSizeType) {
+        mMenuAnimationController.fadeInNowIfEnabled();
+
         mMenuViewAppearance.setSizeType(newSizeType);
 
         mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding());
@@ -106,41 +174,117 @@
 
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
+
+        mMenuAnimationController.fadeOutIfEnabled();
     }
 
     private void onTargetFeaturesChanged(List<AccessibilityTarget> newTargetFeatures) {
         // TODO(b/252756133): Should update specific item instead of the whole list
+        mMenuAnimationController.fadeInNowIfEnabled();
+
         mTargetFeatures.clear();
         mTargetFeatures.addAll(newTargetFeatures);
         mMenuViewAppearance.setTargetFeaturesSize(mTargetFeatures.size());
+        mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode());
         mAdapter.notifyDataSetChanged();
 
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
+
+        mMenuAnimationController.fadeOutIfEnabled();
+    }
+
+    private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
+        mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(),
+                fadeEffectInfo.getOpacity());
+    }
+
+    Rect getMenuDraggableBounds() {
+        return mMenuViewAppearance.getMenuDraggableBounds();
+    }
+
+    void persistPositionAndUpdateEdge(Position percentagePosition) {
+        mMenuViewModel.updateMenuSavingPosition(percentagePosition);
+        mMenuViewAppearance.setPercentagePosition(percentagePosition);
+
+        onEdgeChangedIfNeeded();
+    }
+
+    /**
+     * Uses the touch events from the parent view to identify if users clicked the extra
+     * space of the menu view. If yes, will use the percentage position and update the
+     * translations of the menu view to meet the effect of moving out from the edge. It’s only
+     * used when the menu view is hidden to the screen edge.
+     *
+     * @param x the current x of the touch event from the parent {@link MenuViewLayer} of the
+     * {@link MenuView}.
+     * @param y the current y of the touch event from the parent {@link MenuViewLayer} of the
+     * {@link MenuView}.
+     * @return true if consume the touch event, otherwise false.
+     */
+    boolean maybeMoveOutEdgeAndShow(int x, int y) {
+        // Utilizes the touch region of the parent view to implement that users could tap extra
+        // the space region to show the menu from the edge.
+        if (!mMenuAnimationController.isMovedToEdge() || !mBoundsInParent.contains(x, y)) {
+            return false;
+        }
+
+        mMenuAnimationController.fadeInNowIfEnabled();
+
+        mMenuAnimationController.moveOutEdgeAndShow();
+
+        mMenuAnimationController.fadeOutIfEnabled();
+        return true;
     }
 
     void show() {
+        mMenuViewModel.getPercentagePositionData().observeForever(mPercentagePositionObserver);
+        mMenuViewModel.getFadeEffectInfoData().observeForever(mFadeEffectInfoObserver);
         mMenuViewModel.getTargetFeaturesData().observeForever(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver);
         setVisibility(VISIBLE);
         mMenuViewModel.registerContentObservers();
+        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+        getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
     }
 
     void hide() {
         setVisibility(GONE);
+        mBoundsInParent.setEmpty();
+        mMenuViewModel.getPercentagePositionData().removeObserver(mPercentagePositionObserver);
+        mMenuViewModel.getFadeEffectInfoData().removeObserver(mFadeEffectInfoObserver);
         mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver);
         mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver);
         mMenuViewModel.unregisterContentObservers();
+        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+        getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater);
+    }
+
+    void onDraggingStart() {
+        final int[] insets = mMenuViewAppearance.getMenuMovingStateInsets();
+        getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2],
+                insets[3]);
+
+        final GradientDrawable gradientDrawable = getContainerViewGradient();
+        gradientDrawable.setCornerRadii(mMenuViewAppearance.getMenuMovingStateRadii());
+    }
+
+    void onBoundsInParentChanged(int newLeft, int newTop) {
+        mBoundsInParent.offsetTo(newLeft, newTop);
     }
 
     void loadLayoutResources() {
         mMenuViewAppearance.update();
 
+        mTargetFeaturesView.setContentDescription(mMenuViewAppearance.getContentDescription());
         setBackground(mMenuViewAppearance.getMenuBackground());
         setElevation(mMenuViewAppearance.getMenuElevation());
         onItemSizeChanged();
         onSizeChanged();
         onEdgeChanged();
+        onPositionChanged();
     }
 
     private InstantInsetLayerDrawable getContainerViewInsetLayer() {
@@ -150,4 +294,9 @@
     private GradientDrawable getContainerViewGradient() {
         return (GradientDrawable) getContainerViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
     }
+
+    private void updateSystemGestureExcludeRects() {
+        final ViewGroup parentView = (ViewGroup) getParent();
+        parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent));
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
index b9b7732..034e96a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
@@ -16,12 +16,21 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
+import static android.view.View.OVER_SCROLL_ALWAYS;
+import static android.view.View.OVER_SCROLL_NEVER;
+
 import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance.MenuSizeType.SMALL;
 
 import android.annotation.IntDef;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Insets;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.annotation.DimenRes;
 
@@ -34,9 +43,13 @@
  * Provides the layout resources information of the {@link MenuView}.
  */
 class MenuViewAppearance {
+    private final WindowManager mWindowManager;
     private final Resources mRes;
+    private final Position mPercentagePosition = new Position(/* percentageX= */
+            0f, /* percentageY= */ 0f);
     private int mTargetFeaturesSize;
     private int mSizeType;
+    private int mMargin;
     private int mSmallPadding;
     private int mLargePadding;
     private int mSmallIconSize;
@@ -51,6 +64,7 @@
     private int mElevation;
     private float[] mRadii;
     private Drawable mBackgroundDrawable;
+    private String mContentDescription;
 
     @IntDef({
             SMALL,
@@ -62,13 +76,15 @@
         int LARGE = 1;
     }
 
-    MenuViewAppearance(Context context) {
+    MenuViewAppearance(Context context, WindowManager windowManager) {
+        mWindowManager = windowManager;
         mRes = context.getResources();
 
         update();
     }
 
     void update() {
+        mMargin = mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_margin);
         mSmallPadding =
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_padding);
         mLargePadding =
@@ -81,7 +97,7 @@
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_small_single_radius);
         mSmallMultipleRadius = mRes.getDimensionPixelSize(
                 R.dimen.accessibility_floating_menu_small_multiple_radius);
-        mRadii = createRadii(getMenuRadius(mTargetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
         mLargeSingleRadius =
                 mRes.getDimensionPixelSize(R.dimen.accessibility_floating_menu_large_single_radius);
         mLargeMultipleRadius = mRes.getDimensionPixelSize(
@@ -93,18 +109,59 @@
         final Drawable drawable =
                 mRes.getDrawable(R.drawable.accessibility_floating_menu_background);
         mBackgroundDrawable = new InstantInsetLayerDrawable(new Drawable[]{drawable});
+        mContentDescription = mRes.getString(
+                com.android.internal.R.string.accessibility_select_shortcut_menu_title);
     }
 
     void setSizeType(int sizeType) {
         mSizeType = sizeType;
 
-        mRadii = createRadii(getMenuRadius(mTargetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
     }
 
     void setTargetFeaturesSize(int targetFeaturesSize) {
         mTargetFeaturesSize = targetFeaturesSize;
 
-        mRadii = createRadii(getMenuRadius(targetFeaturesSize));
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(targetFeaturesSize));
+    }
+
+    void setPercentagePosition(Position percentagePosition) {
+        mPercentagePosition.update(percentagePosition);
+
+        mRadii = createRadii(isMenuOnLeftSide(), getMenuRadius(mTargetFeaturesSize));
+    }
+
+    Rect getMenuDraggableBounds() {
+        final int margin = getMenuMargin();
+        final Rect draggableBounds = getWindowAvailableBounds();
+
+        // Initializes start position for mapping the translation of the menu view.
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Insets displayCutoutInsets = windowInsets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.displayCutout());
+        draggableBounds.offset(-displayCutoutInsets.left, -displayCutoutInsets.top);
+
+        draggableBounds.top += margin;
+        draggableBounds.right -= getMenuWidth();
+        draggableBounds.bottom -= Math.min(
+                getWindowAvailableBounds().height() - draggableBounds.top,
+                calculateActualMenuHeight() + margin);
+        return draggableBounds;
+    }
+
+    PointF getMenuPosition() {
+        final Rect draggableBounds = getMenuDraggableBounds();
+
+        return new PointF(
+                draggableBounds.left
+                        + draggableBounds.width() * mPercentagePosition.getPercentageX(),
+                draggableBounds.top
+                        + draggableBounds.height() * mPercentagePosition.getPercentageY());
+    }
+
+    String getContentDescription() {
+        return mContentDescription;
     }
 
     Drawable getMenuBackground() {
@@ -115,20 +172,41 @@
         return mElevation;
     }
 
+    int getMenuWidth() {
+        return getMenuPadding() * 2 + getMenuIconSize();
+    }
+
     int getMenuHeight() {
-        return calculateActualMenuHeight();
+        return Math.min(getWindowAvailableBounds().height() - mMargin * 2,
+                calculateActualMenuHeight());
     }
 
     int getMenuIconSize() {
         return mSizeType == SMALL ? mSmallIconSize : mLargeIconSize;
     }
 
+    private int getMenuMargin() {
+        return mMargin;
+    }
+
     int getMenuPadding() {
         return mSizeType == SMALL ? mSmallPadding : mLargePadding;
     }
 
     int[] getMenuInsets() {
-        return new int[]{mInset, 0, 0, 0};
+        final int left = isMenuOnLeftSide() ? mInset : 0;
+        final int right = isMenuOnLeftSide() ? 0 : mInset;
+
+        return new int[]{left, 0, right, 0};
+    }
+
+    int[] getMenuMovingStateInsets() {
+        return new int[]{0, 0, 0, 0};
+    }
+
+    float[] getMenuMovingStateRadii() {
+        final float radius = getMenuRadius(mTargetFeaturesSize);
+        return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
     }
 
     int getMenuStrokeWidth() {
@@ -147,6 +225,14 @@
         return mSizeType == SMALL ? getSmallSize(itemCount) : getLargeSize(itemCount);
     }
 
+    int getMenuScrollMode() {
+        return hasExceededMaxWindowHeight() ? OVER_SCROLL_ALWAYS : OVER_SCROLL_NEVER;
+    }
+
+    private boolean hasExceededMaxWindowHeight() {
+        return calculateActualMenuHeight() > getWindowAvailableBounds().height();
+    }
+
     @DimenRes
     private int getSmallSize(int itemCount) {
         return itemCount > 1 ? mSmallMultipleRadius : mSmallSingleRadius;
@@ -157,8 +243,29 @@
         return itemCount > 1 ? mLargeMultipleRadius : mLargeSingleRadius;
     }
 
-    private static float[] createRadii(float radius) {
-        return new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f};
+    private static float[] createRadii(boolean isMenuOnLeftSide, float radius) {
+        return isMenuOnLeftSide
+                ? new float[]{0.0f, 0.0f, radius, radius, radius, radius, 0.0f, 0.0f}
+                : new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
+    }
+
+    private Rect getWindowAvailableBounds() {
+        final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
+        final WindowInsets windowInsets = windowMetrics.getWindowInsets();
+        final Insets insets = windowInsets.getInsetsIgnoringVisibility(
+                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
+
+        final Rect bounds = new Rect(windowMetrics.getBounds());
+        bounds.left += insets.left;
+        bounds.right -= insets.right;
+        bounds.top += insets.top;
+        bounds.bottom -= insets.bottom;
+
+        return bounds;
+    }
+
+    private boolean isMenuOnLeftSide() {
+        return mPercentagePosition.getPercentageX() < 0.5f;
     }
 
     private int calculateActualMenuHeight() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 4ea2f77..5252519 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -19,6 +19,8 @@
 import android.annotation.IntDef;
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.view.MotionEvent;
+import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
@@ -41,17 +43,27 @@
         int MENU_VIEW = 0;
     }
 
-    MenuViewLayer(@NonNull Context context) {
+    MenuViewLayer(@NonNull Context context, WindowManager windowManager) {
         super(context);
 
         final MenuViewModel menuViewModel = new MenuViewModel(context);
-        final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context);
+        final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context,
+                windowManager);
         mMenuView = new MenuView(context, menuViewModel, menuViewAppearance);
 
         addView(mMenuView, LayerIndex.MENU_VIEW);
     }
 
     @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) {
+            return true;
+        }
+
+        return super.onInterceptTouchEvent(event);
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
index 1e15a59..d2093c2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.graphics.PixelFormat;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 
 /**
@@ -33,7 +34,7 @@
 
     MenuViewLayerController(Context context, WindowManager windowManager) {
         mWindowManager = windowManager;
-        mMenuViewLayer = new MenuViewLayer(context);
+        mMenuViewLayer = new MenuViewLayer(context, windowManager);
     }
 
     @Override
@@ -68,9 +69,10 @@
                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
                 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                 PixelFormat.TRANSLUCENT);
+        params.receiveInsetsIgnoringZOrder = true;
         params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
         params.windowAnimations = android.R.style.Animation_Translucent;
-
+        params.setFitInsetsTypes(WindowInsets.Type.navigationBars());
         return params;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
index c3ba439..e8a2b6e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java
@@ -33,6 +33,9 @@
     private final MutableLiveData<List<AccessibilityTarget>> mTargetFeaturesData =
             new MutableLiveData<>();
     private final MutableLiveData<Integer> mSizeTypeData = new MutableLiveData<>();
+    private final MutableLiveData<MenuFadeEffectInfo> mFadeEffectInfoData =
+            new MutableLiveData<>();
+    private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>();
     private final MenuInfoRepository mInfoRepository;
 
     MenuViewModel(Context context) {
@@ -49,11 +52,30 @@
         mSizeTypeData.setValue(newSizeType);
     }
 
+    @Override
+    public void onFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) {
+        mFadeEffectInfoData.setValue(fadeEffectInfo);
+    }
+
+    void updateMenuSavingPosition(Position percentagePosition) {
+        mInfoRepository.updateMenuSavingPosition(percentagePosition);
+    }
+
+    LiveData<Position> getPercentagePositionData() {
+        mInfoRepository.loadMenuPosition(mPercentagePositionData::setValue);
+        return mPercentagePositionData;
+    }
+
     LiveData<Integer> getSizeTypeData() {
         mInfoRepository.loadMenuSizeType(mSizeTypeData::setValue);
         return mSizeTypeData;
     }
 
+    LiveData<MenuFadeEffectInfo> getFadeEffectInfoData() {
+        mInfoRepository.loadMenuFadeEffectInfo(mFadeEffectInfoData::setValue);
+        return mFadeEffectInfoData;
+    }
+
     LiveData<List<AccessibilityTarget>> getTargetFeaturesData() {
         mInfoRepository.loadMenuTargetFeatures(mTargetFeaturesData::setValue);
         return mTargetFeaturesData;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
index 7b7eda8..fc21be2 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/Position.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import android.annotation.FloatRange;
+import android.annotation.NonNull;
 import android.text.TextUtils;
 
 /**
@@ -62,6 +63,13 @@
     }
 
     /**
+     * Updates the position with {@code percentagePosition}.
+     */
+    public void update(@NonNull Position percentagePosition) {
+        update(percentagePosition.getPercentageX(), percentagePosition.getPercentageY());
+    }
+
+    /**
      * Updates the position with {@code percentageX} and {@code percentageY}.
      *
      * @param percentageX the new percentage of X-axis of the screen, from 0.0 to 1.0.
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index 6b85976..6785a43 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -103,6 +103,7 @@
             AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
             AppOpsManager.OP_RECORD_AUDIO,
             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+            AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
             AppOpsManager.OP_COARSE_LOCATION,
             AppOpsManager.OP_FINE_LOCATION
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 3e796cd0..b50bfd7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -127,7 +127,7 @@
     private final ScrollView mBiometricScrollView;
     private final View mPanelView;
     private final float mTranslationY;
-    @ContainerState private int mContainerState = STATE_UNKNOWN;
+    @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
     private final Set<Integer> mFailedModalities = new HashSet<Integer>();
     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
 
@@ -630,11 +630,25 @@
         wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
     }
 
+    private void forceExecuteAnimatedIn() {
+        if (mContainerState == STATE_ANIMATING_IN) {
+            //clear all animators
+            if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+                mCredentialView.animate().cancel();
+            }
+            mPanelView.animate().cancel();
+            mBiometricView.animate().cancel();
+            animate().cancel();
+            onDialogAnimatedIn();
+        }
+    }
+
     @Override
     public void dismissWithoutCallback(boolean animate) {
         if (animate) {
             animateAway(false /* sendReason */, 0 /* reason */);
         } else {
+            forceExecuteAnimatedIn();
             removeWindowIfAttached();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 9493975..8c7e0ef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -652,17 +652,6 @@
         mUdfpsController.onAodInterrupt(screenX, screenY, major, minor);
     }
 
-    /**
-     * Cancel a fingerprint scan manually. This will get rid of the white circle on the udfps
-     * sensor area even if the user hasn't explicitly lifted their finger yet.
-     */
-    public void onCancelUdfps() {
-        if (mUdfpsController == null) {
-            return;
-        }
-        mUdfpsController.onCancelUdfps();
-    }
-
     private void sendResultAndCleanUp(@DismissedReason int reason,
             @Nullable byte[] credentialAttestation) {
         if (mReceiver == null) {
@@ -1021,8 +1010,6 @@
         } else {
             Log.w(TAG, "onBiometricError callback but dialog is gone");
         }
-
-        onCancelUdfps();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
index 5ed8986..76cd3f4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java
@@ -24,6 +24,7 @@
 import android.graphics.Insets;
 import android.os.UserHandle;
 import android.text.InputType;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.View;
@@ -151,39 +152,52 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
 
-        if (mAuthCredentialInput == null || mAuthCredentialHeader == null
-                || mSubtitleView == null || mPasswordField == null || mErrorView == null) {
+        if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null
+                || mDescriptionView == null || mPasswordField == null || mErrorView == null) {
             return;
         }
 
-        // b/157910732 In AuthContainerView#getLayoutParams() we used to prevent jank risk when
-        // resizing by IME show or hide, we used to setFitInsetsTypes `~WindowInsets.Type.ime()` to
-        // LP. As a result this view needs to listen onApplyWindowInsets() and handle onLayout.
         int inputLeftBound;
         int inputTopBound;
         int headerRightBound = right;
+        int headerTopBounds = top;
+        final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom()
+                : mSubtitleView.getBottom();
+        final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom
+                : mDescriptionView.getBottom();
         if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            inputTopBound = (bottom - (mPasswordField.getHeight() + mErrorView.getHeight())) / 2;
+            inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2;
             inputLeftBound = (right - left) / 2;
             headerRightBound = inputLeftBound;
+            headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset);
         } else {
-            inputTopBound = mSubtitleView.getBottom() + (bottom - mSubtitleView.getBottom()) / 2;
+            inputTopBound =
+                    descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2;
             inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2;
         }
 
-        mAuthCredentialHeader.layout(left, top, headerRightBound, bottom);
+        if (mDescriptionView.getBottom() > mBottomInset) {
+            mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom);
+        }
         mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom);
     }
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        final int newWidth = MeasureSpec.getSize(widthMeasureSpec);
         final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset;
 
-        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), newHeight);
+        setMeasuredDimension(newWidth, newHeight);
 
-        measureChildren(widthMeasureSpec,
-                MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.AT_MOST));
+        final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2,
+                MeasureSpec.AT_MOST);
+        final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED);
+        if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+            measureChildren(halfWidthSpec, fullHeightSpec);
+        } else {
+            measureChildren(widthMeasureSpec, fullHeightSpec);
+        }
     }
 
     @NonNull
@@ -193,6 +207,20 @@
         final Insets bottomInset = insets.getInsets(ime());
         if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) {
             mBottomInset = bottomInset.bottom;
+            if (mBottomInset > 0
+                    && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+                mTitleView.setSingleLine(true);
+                mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE);
+                mTitleView.setMarqueeRepeatLimit(-1);
+                // select to enable marquee unless a screen reader is enabled
+                mTitleView.setSelected(!mAccessibilityManager.isEnabled()
+                        || !mAccessibilityManager.isTouchExplorationEnabled());
+            } else {
+                mTitleView.setSingleLine(false);
+                mTitleView.setEllipsize(null);
+                // select to enable marquee unless a screen reader is enabled
+                mTitleView.setSelected(false);
+            }
             requestLayout();
         }
         return insets;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
index 11498db..f9e44a0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java
@@ -93,7 +93,9 @@
     @Override
     protected void onErrorTimeoutFinish() {
         super.onErrorTimeoutFinish();
-        mLockPatternView.setEnabled(true);
+        // select to enable marquee unless a screen reader is enabled
+        mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled()
+                || !mAccessibilityManager.isTouchExplorationEnabled());
     }
 
     public AuthCredentialPatternView(Context context, AttributeSet attrs) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
index d4176ac..fa623d1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java
@@ -77,7 +77,7 @@
     protected final Handler mHandler;
     protected final LockPatternUtils mLockPatternUtils;
 
-    private final AccessibilityManager mAccessibilityManager;
+    protected final AccessibilityManager mAccessibilityManager;
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
 
@@ -86,10 +86,10 @@
     private boolean mShouldAnimatePanel;
     private boolean mShouldAnimateContents;
 
-    private TextView mTitleView;
+    protected TextView mTitleView;
     protected TextView mSubtitleView;
-    private TextView mDescriptionView;
-    private ImageView mIconView;
+    protected TextView mDescriptionView;
+    protected ImageView mIconView;
     protected TextView mErrorView;
 
     protected @Utils.CredentialType int mCredentialType;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 0f5a99c..3273d74 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -788,7 +788,7 @@
             // ACTION_UP/ACTION_CANCEL,  we need to be careful about not letting the screen
             // accidentally remain in high brightness mode. As a mitigation, queue a call to
             // cancel the fingerprint scan.
-            mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::onCancelUdfps,
+            mCancelAodTimeoutAction = mFgExecutor.executeDelayed(this::cancelAodInterrupt,
                     AOD_INTERRUPT_TIMEOUT_MILLIS);
             // using a hard-coded value for major and minor until it is available from the sensor
             onFingerDown(requestId, screenX, screenY, minor, major);
@@ -815,26 +815,22 @@
     }
 
     /**
-     * Cancel UDFPS affordances - ability to hide the UDFPS overlay before the user explicitly
-     * lifts their finger. Generally, this should be called on errors in the authentication flow.
-     *
-     * The sensor that triggers an AOD fingerprint interrupt (see onAodInterrupt) doesn't give
-     * ACTION_UP/ACTION_CANCEL events, so and AOD interrupt scan needs to be cancelled manually.
+     * The sensor that triggers {@link #onAodInterrupt} doesn't emit ACTION_UP or ACTION_CANCEL
+     * events, which means the fingerprint gesture created by the AOD interrupt needs to be
+     * cancelled manually.
      * This should be called when authentication either succeeds or fails. Failing to cancel the
      * scan will leave the display in the UDFPS mode until the user lifts their finger. On optical
      * sensors, this can result in illumination persisting for longer than necessary.
      */
-    void onCancelUdfps() {
+    @VisibleForTesting
+    void cancelAodInterrupt() {
         if (!mIsAodInterruptActive) {
             return;
         }
         if (mOverlay != null && mOverlay.getOverlayView() != null) {
             onFingerUp(mOverlay.getRequestId(), mOverlay.getOverlayView());
         }
-        if (mCancelAodTimeoutAction != null) {
-            mCancelAodTimeoutAction.run();
-            mCancelAodTimeoutAction = null;
-        }
+        mCancelAodTimeoutAction = null;
         mIsAodInterruptActive = false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 66a521c..7d01096 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -21,13 +21,18 @@
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
-import android.hardware.biometrics.BiometricOverlayConstants
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
+import android.os.Build
 import android.os.RemoteException
+import android.provider.Settings
 import android.util.Log
 import android.util.RotationUtils
 import android.view.LayoutInflater
@@ -38,6 +43,7 @@
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
 import androidx.annotation.LayoutRes
+import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
@@ -54,13 +60,16 @@
 
 private const val TAG = "UdfpsControllerOverlay"
 
+@VisibleForTesting
+const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
+
 /**
  * Keeps track of the overlay state and UI resources associated with a single FingerprintService
  * request. This state can persist across configuration changes via the [show] and [hide]
  * methods.
  */
 @UiThread
-class UdfpsControllerOverlay(
+class UdfpsControllerOverlay @JvmOverloads constructor(
     private val context: Context,
     fingerprintManager: FingerprintManager,
     private val inflater: LayoutInflater,
@@ -82,7 +91,8 @@
     @ShowReason val requestReason: Int,
     private val controllerCallback: IUdfpsOverlayControllerCallback,
     private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
-    private val activityLaunchAnimator: ActivityLaunchAnimator
+    private val activityLaunchAnimator: ActivityLaunchAnimator,
+    private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -102,18 +112,19 @@
         gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
         flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
-          WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
+                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
         privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
         // Avoid announcing window title.
         accessibilityTitle = " "
     }
 
     /** A helper if the [requestReason] was due to enrollment. */
-    val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) {
-        UdfpsEnrollHelper(context, fingerprintManager, requestReason)
-    } else {
-        null
-    }
+    val enrollHelper: UdfpsEnrollHelper? =
+        if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) {
+            UdfpsEnrollHelper(context, fingerprintManager, requestReason)
+        } else {
+            null
+        }
 
     /** If the overlay is currently showing. */
     val isShowing: Boolean
@@ -129,6 +140,17 @@
 
     private var touchExplorationEnabled = false
 
+    private fun shouldRemoveEnrollmentUi(): Boolean {
+        if (isDebuggable) {
+            return Settings.Global.getInt(
+                context.contentResolver,
+                SETTING_REMOVE_ENROLLMENT_UI,
+                0 /* def */
+            ) != 0
+        }
+        return false
+    }
+
     /** Show the overlay or return false and do nothing if it is already showing. */
     @SuppressLint("ClickableViewAccessibility")
     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
@@ -183,7 +205,18 @@
         view: UdfpsView,
         controller: UdfpsController
     ): UdfpsAnimationViewController<*>? {
-        return when (requestReason) {
+        val isEnrollment = when (requestReason) {
+            REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
+            else -> false
+        }
+
+        val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
+            REASON_AUTH_OTHER
+        } else {
+            requestReason
+        }
+
+        return when (filteredRequestReason) {
             REASON_ENROLL_FIND_SENSOR,
             REASON_ENROLL_ENROLLING -> {
                 UdfpsEnrollViewController(
@@ -198,7 +231,7 @@
                     overlayParams.scaleFactor
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> {
+            REASON_AUTH_KEYGUARD -> {
                 UdfpsKeyguardViewController(
                     view.addUdfpsView(R.layout.udfps_keyguard_view),
                     statusBarStateController,
@@ -216,7 +249,7 @@
                     activityLaunchAnimator
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_BP -> {
+            REASON_AUTH_BP -> {
                 // note: empty controller, currently shows no visual affordance
                 UdfpsBpViewController(
                     view.addUdfpsView(R.layout.udfps_bp_view),
@@ -226,8 +259,8 @@
                     dumpManager
                 )
             }
-            BiometricOverlayConstants.REASON_AUTH_OTHER,
-            BiometricOverlayConstants.REASON_AUTH_SETTINGS -> {
+            REASON_AUTH_OTHER,
+            REASON_AUTH_SETTINGS -> {
                 UdfpsFpmOtherViewController(
                     view.addUdfpsView(R.layout.udfps_fpm_other_view),
                     statusBarStateController,
@@ -440,4 +473,4 @@
 private fun Int.isImportantForAccessibility() =
     this == REASON_ENROLL_FIND_SENSOR ||
             this == REASON_ENROLL_ENROLLING ||
-            this == BiometricOverlayConstants.REASON_AUTH_BP
+            this == REASON_AUTH_BP
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
index 9b7d498..8e062bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/BroadcastDialog.java
@@ -17,15 +17,11 @@
 package com.android.systemui.bluetooth;
 
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
 import android.os.Bundle;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.Window;
-import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.TextView;
 
@@ -33,7 +29,7 @@
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
-import com.android.systemui.media.MediaDataUtils;
+import com.android.systemui.media.controls.util.MediaDataUtils;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
index 3871248..858bac3 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollector.java
@@ -44,9 +44,6 @@
     void onQsDown();
 
     /** */
-    void setQsExpanded(boolean expanded);
-
-    /** */
     boolean shouldEnforceBouncer();
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
index 28aac05..0b7d6ab 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorFake.java
@@ -49,10 +49,6 @@
     }
 
     @Override
-    public void setQsExpanded(boolean expanded) {
-    }
-
-    @Override
     public boolean shouldEnforceBouncer() {
         return false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index f5f9655..da3d293 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -23,6 +23,8 @@
 import android.util.Log;
 import android.view.MotionEvent;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.systemui.dagger.SysUISingleton;
@@ -30,6 +32,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
@@ -133,6 +136,7 @@
             ProximitySensor proximitySensor,
             StatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
+            ShadeExpansionStateManager shadeExpansionStateManager,
             BatteryController batteryController,
             DockManager dockManager,
             @Main DelayableExecutor mainExecutor,
@@ -157,6 +161,8 @@
 
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateCallback);
 
+        shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
+
         mBatteryController.addCallback(mBatteryListener);
         mDockManager.addListener(mDockEventListener);
     }
@@ -193,8 +199,8 @@
     public void onQsDown() {
     }
 
-    @Override
-    public void setQsExpanded(boolean expanded) {
+    @VisibleForTesting
+    void onQsExpansionChanged(Boolean expanded) {
         if (expanded) {
             unregisterSensors();
         } else if (mSessionStarted) {
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index bfb27a4..9f338d1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -459,7 +459,7 @@
         anim.start();
     }
 
-    private void hideImmediate() {
+    void hideImmediate() {
         // Note this may be called multiple times if multiple dismissal events happen at the same
         // time.
         mTimeoutHandler.cancelTimeout();
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
index bebade0..08e8293 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/ContentDescription.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.common.shared.model
 
 import android.annotation.StringRes
+import android.content.Context
 
 /**
  * Models a content description, that can either be already [loaded][ContentDescription.Loaded] or
@@ -30,4 +31,20 @@
     data class Resource(
         @StringRes val res: Int,
     ) : ContentDescription()
+
+    companion object {
+        /**
+         * Returns the loaded content description string, or null if we don't have one.
+         *
+         * Prefer [com.android.systemui.common.ui.binder.ContentDescriptionViewBinder.bind] over
+         * this method. This should only be used for testing or concatenation purposes.
+         */
+        fun ContentDescription?.loadContentDescription(context: Context): String? {
+            return when (this) {
+                null -> null
+                is Loaded -> this.description
+                is Resource -> context.getString(this.res)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
index 5d0e08f..4a56932 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Text.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.common.shared.model
 
 import android.annotation.StringRes
+import android.content.Context
 
 /**
  * Models a text, that can either be already [loaded][Text.Loaded] or be a [reference]
@@ -31,4 +32,20 @@
     data class Resource(
         @StringRes val res: Int,
     ) : Text()
+
+    companion object {
+        /**
+         * Returns the loaded test string, or null if we don't have one.
+         *
+         * Prefer [com.android.systemui.common.ui.binder.TextViewBinder.bind] over this method. This
+         * should only be used for testing or concatenation purposes.
+         */
+        fun Text?.loadText(context: Context): String? {
+            return when (this) {
+                null -> null
+                is Loaded -> this.text
+                is Resource -> context.getString(this.res)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index d7638d6..7e31626 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -23,7 +23,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.clock.ClockModule;
+import com.android.keyguard.clock.ClockInfoModule;
+import com.android.keyguard.dagger.ClockRegistryModule;
 import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.BootCompleteCache;
 import com.android.systemui.BootCompleteCacheImpl;
@@ -120,7 +121,8 @@
             BiometricsModule.class,
             BouncerViewModule.class,
             ClipboardOverlayModule.class,
-            ClockModule.class,
+            ClockInfoModule.class,
+            ClockRegistryModule.class,
             CoroutinesModule.class,
             DreamModule.class,
             ControlsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 5fdd198..976afd4 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,8 +34,6 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -47,15 +45,13 @@
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Main private val mainExecutor: Executor,
-    private val featureFlags: FeatureFlags
 ) : DecorProviderFactory() {
     private val display = context.display
     private val displayInfo = DisplayInfo()
 
     override val hasProviders: Boolean
         get() {
-            if (!featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) ||
-                    authController.faceSensorLocation == null) {
+            if (authController.faceSensorLocation == null) {
                 return false
             }
 
@@ -99,7 +95,7 @@
     }
 
     fun shouldShowFaceScanningAnim(): Boolean {
-        return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning
+        return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceDetectionRunning
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
index e18c0e1..8cfd391 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
@@ -33,7 +33,7 @@
  * of the privacy dot views are controlled by the PrivacyDotViewController.
  */
 @SysUISingleton
-class PrivacyDotDecorProviderFactory @Inject constructor(
+open class PrivacyDotDecorProviderFactory @Inject constructor(
     @Main private val res: Resources
 ) : DecorProviderFactory() {
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 2e51b51..b69afeb 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -287,8 +287,8 @@
     /**
      * Appends sensor event dropped event to logs
      */
-    public void traceSensorEventDropped(int sensorEvent, String reason) {
-        mLogger.logSensorEventDropped(sensorEvent, reason);
+    public void traceSensorEventDropped(@Reason int pulseReason, String reason) {
+        mLogger.logSensorEventDropped(pulseReason, reason);
     }
 
     /**
@@ -386,6 +386,47 @@
         mLogger.logSetAodDimmingScrim((long) scrimOpacity);
     }
 
+    /**
+     * Appends sensor attempted to register and whether it was a successful registration.
+     */
+    public void traceSensorRegisterAttempt(String sensorName, boolean successfulRegistration) {
+        mLogger.logSensorRegisterAttempt(sensorName, successfulRegistration);
+    }
+
+    /**
+     * Appends sensor attempted to unregister and whether it was successfully unregistered.
+     */
+    public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered) {
+        mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered);
+    }
+
+    /**
+     * Appends sensor attempted to unregister and whether it was successfully unregistered
+     * with a reason the sensor is being unregistered.
+     */
+    public void traceSensorUnregisterAttempt(String sensorInfo, boolean successfullyUnregistered,
+            String reason) {
+        mLogger.logSensorUnregisterAttempt(sensorInfo, successfullyUnregistered, reason);
+    }
+
+    /**
+     * Appends the event of skipping a sensor registration since it's already registered.
+     */
+    public void traceSkipRegisterSensor(String sensorInfo) {
+        mLogger.logSkipSensorRegistration(sensorInfo);
+    }
+
+    /**
+     * Appends a plugin sensor was registered or unregistered event.
+     */
+    public void tracePluginSensorUpdate(boolean registered) {
+        if (registered) {
+            mLogger.log("register plugin sensor");
+        } else {
+            mLogger.log("unregister plugin sensor");
+        }
+    }
+
     private class SummaryStats {
         private int mCount;
 
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
index 0e1bfba..18c8e01 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLogger.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.log.dagger.DozeLog
 import com.android.systemui.statusbar.policy.DevicePostureController
+import com.google.errorprone.annotations.CompileTimeConstant
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
@@ -224,10 +225,14 @@
         })
     }
 
-    fun logPulseDropped(from: String, state: DozeMachine.State) {
+    /**
+     * Log why a pulse was dropped and the current doze machine state. The state can be null
+     * if the DozeMachine is the middle of transitioning between states.
+     */
+    fun logPulseDropped(from: String, state: DozeMachine.State?) {
         buffer.log(TAG, INFO, {
             str1 = from
-            str2 = state.name
+            str2 = state?.name
         }, {
             "Pulse dropped, cannot pulse from=$str1 state=$str2"
         })
@@ -320,6 +325,50 @@
             "Doze car mode started"
         })
     }
+
+    fun logSensorRegisterAttempt(sensorInfo: String, successfulRegistration: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = sensorInfo
+            bool1 = successfulRegistration
+        }, {
+            "Register sensor. Success=$bool1 sensor=$str1"
+        })
+    }
+
+    fun logSensorUnregisterAttempt(sensorInfo: String, successfulUnregister: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = sensorInfo
+            bool1 = successfulUnregister
+        }, {
+            "Unregister sensor. Success=$bool1 sensor=$str1"
+        })
+    }
+
+    fun logSensorUnregisterAttempt(
+            sensorInfo: String,
+            successfulUnregister: Boolean,
+            reason: String
+    ) {
+        buffer.log(TAG, INFO, {
+            str1 = sensorInfo
+            bool1 = successfulUnregister
+            str2 = reason
+        }, {
+            "Unregister sensor. reason=$str2. Success=$bool1 sensor=$str1"
+        })
+    }
+
+    fun logSkipSensorRegistration(sensor: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = sensor
+        }, {
+            "Skipping sensor registration because its already registered. sensor=$str1"
+        })
+    }
+
+    fun log(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, DEBUG, msg)
+    }
 }
 
 private const val TAG = "DozeLog"
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
index ae41215..96c35d4 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java
@@ -20,7 +20,6 @@
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 
 import android.annotation.MainThread;
-import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Trace;
@@ -145,10 +144,9 @@
 
     private final Service mDozeService;
     private final WakeLock mWakeLock;
-    private final AmbientDisplayConfiguration mConfig;
+    private final AmbientDisplayConfiguration mAmbientDisplayConfig;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final DozeHost mDozeHost;
-    private final UiModeManager mUiModeManager;
     private final DockManager mDockManager;
     private final Part[] mParts;
 
@@ -156,18 +154,18 @@
     private State mState = State.UNINITIALIZED;
     private int mPulseReason;
     private boolean mWakeLockHeldForCurrentState = false;
+    private int mUiModeType = Configuration.UI_MODE_TYPE_NORMAL;
 
     @Inject
-    public DozeMachine(@WrappedService Service service, AmbientDisplayConfiguration config,
+    public DozeMachine(@WrappedService Service service,
+            AmbientDisplayConfiguration ambientDisplayConfig,
             WakeLock wakeLock, WakefulnessLifecycle wakefulnessLifecycle,
-            UiModeManager uiModeManager,
             DozeLog dozeLog, DockManager dockManager,
             DozeHost dozeHost, Part[] parts) {
         mDozeService = service;
-        mConfig = config;
+        mAmbientDisplayConfig = ambientDisplayConfig;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mWakeLock = wakeLock;
-        mUiModeManager = uiModeManager;
         mDozeLog = dozeLog;
         mDockManager = dockManager;
         mDozeHost = dozeHost;
@@ -187,6 +185,18 @@
     }
 
     /**
+     * Notifies the {@link DozeMachine} that {@link Configuration} has changed.
+     */
+    public void onConfigurationChanged(Configuration newConfiguration) {
+        int newUiModeType = newConfiguration.uiMode & Configuration.UI_MODE_TYPE_MASK;
+        if (mUiModeType == newUiModeType) return;
+        mUiModeType = newUiModeType;
+        for (Part part : mParts) {
+            part.onUiModeTypeChanged(mUiModeType);
+        }
+    }
+
+    /**
      * Requests transitioning to {@code requestedState}.
      *
      * This can be called during a state transition, in which case it will be queued until all
@@ -211,6 +221,14 @@
         requestState(State.DOZE_REQUEST_PULSE, pulseReason);
     }
 
+    /**
+     * @return true if {@link DozeMachine} is currently in either {@link State#UNINITIALIZED}
+     *  or {@link State#FINISH}
+     */
+    public boolean isUninitializedOrFinished() {
+        return mState == State.UNINITIALIZED || mState == State.FINISH;
+    }
+
     void onScreenState(int state) {
         mDozeLog.traceDisplayState(state);
         for (Part part : mParts) {
@@ -360,7 +378,7 @@
         if (mState == State.FINISH) {
             return State.FINISH;
         }
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR
+        if (mUiModeType == Configuration.UI_MODE_TYPE_CAR
                 && (requestedState.canPulse() || requestedState.staysAwake())) {
             Log.i(TAG, "Doze is suppressed with all triggers disabled as car mode is active");
             mDozeLog.traceCarModeStarted();
@@ -411,7 +429,7 @@
                     nextState = State.FINISH;
                 } else if (mDockManager.isDocked()) {
                     nextState = mDockManager.isHidden() ? State.DOZE : State.DOZE_AOD_DOCKED;
-                } else if (mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
+                } else if (mAmbientDisplayConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)) {
                     nextState = State.DOZE_AOD;
                 } else {
                     nextState = State.DOZE;
@@ -427,6 +445,7 @@
     /** Dumps the current state */
     public void dump(PrintWriter pw) {
         pw.print(" state="); pw.println(mState);
+        pw.print(" mUiModeType="); pw.println(mUiModeType);
         pw.print(" wakeLockHeldForCurrentState="); pw.println(mWakeLockHeldForCurrentState);
         pw.print(" wakeLock="); pw.println(mWakeLock);
         pw.println("Parts:");
@@ -459,6 +478,19 @@
 
         /** Sets the {@link DozeMachine} when this Part is associated with one. */
         default void setDozeMachine(DozeMachine dozeMachine) {}
+
+        /**
+         * Notifies the Part about a change in {@link Configuration#uiMode}.
+         *
+         * @param newUiModeType {@link Configuration#UI_MODE_TYPE_NORMAL},
+         *                   {@link Configuration#UI_MODE_TYPE_DESK},
+         *                   {@link Configuration#UI_MODE_TYPE_CAR},
+         *                   {@link Configuration#UI_MODE_TYPE_TELEVISION},
+         *                   {@link Configuration#UI_MODE_TYPE_APPLIANCE},
+         *                   {@link Configuration#UI_MODE_TYPE_WATCH},
+         *                   or {@link Configuration#UI_MODE_TYPE_VR_HEADSET}
+         */
+        default void onUiModeTypeChanged(int newUiModeType) {}
     }
 
     /** A wrapper interface for {@link android.service.dreams.DreamService} */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 997a6e5..d0258d3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -25,7 +25,6 @@
 
 import android.annotation.AnyThread;
 import android.app.ActivityManager;
-import android.content.Context;
 import android.database.ContentObserver;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
@@ -40,7 +39,6 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.IndentingPrintWriter;
-import android.util.Log;
 import android.view.Display;
 
 import androidx.annotation.NonNull;
@@ -91,12 +89,9 @@
  * trigger callbacks on the provided {@link mProxCallback}.
  */
 public class DozeSensors {
-
-    private static final boolean DEBUG = DozeService.DEBUG;
     private static final String TAG = "DozeSensors";
     private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
 
-    private final Context mContext;
     private final AsyncSensorManager mSensorManager;
     private final AmbientDisplayConfiguration mConfig;
     private final WakeLock mWakeLock;
@@ -147,7 +142,6 @@
     }
 
     DozeSensors(
-            Context context,
             AsyncSensorManager sensorManager,
             DozeParameters dozeParameters,
             AmbientDisplayConfiguration config,
@@ -160,7 +154,6 @@
             AuthController authController,
             DevicePostureController devicePostureController
     ) {
-        mContext = context;
         mSensorManager = sensorManager;
         mConfig = config;
         mWakeLock = wakeLock;
@@ -608,10 +601,7 @@
             // cancel the previous sensor:
             if (mRegistered) {
                 final boolean rt = mSensorManager.cancelTriggerSensor(this, oldSensor);
-                if (DEBUG) {
-                    Log.d(TAG, "posture changed, cancelTriggerSensor[" + oldSensor + "] "
-                            + rt);
-                }
+                mDozeLog.traceSensorUnregisterAttempt(oldSensor.toString(), rt, "posture changed");
                 mRegistered = false;
             }
 
@@ -657,19 +647,13 @@
             if (mRequested && !mDisabled && (enabledBySetting() || mIgnoresSetting)) {
                 if (!mRegistered) {
                     mRegistered = mSensorManager.requestTriggerSensor(this, sensor);
-                    if (DEBUG) {
-                        Log.d(TAG, "requestTriggerSensor[" + sensor + "] " + mRegistered);
-                    }
+                    mDozeLog.traceSensorRegisterAttempt(sensor.toString(), mRegistered);
                 } else {
-                    if (DEBUG) {
-                        Log.d(TAG, "requestTriggerSensor[" + sensor + "] already registered");
-                    }
+                    mDozeLog.traceSkipRegisterSensor(sensor.toString());
                 }
             } else if (mRegistered) {
                 final boolean rt = mSensorManager.cancelTriggerSensor(this, sensor);
-                if (DEBUG) {
-                    Log.d(TAG, "cancelTriggerSensor[" + sensor + "] " + rt);
-                }
+                mDozeLog.traceSensorUnregisterAttempt(sensor.toString(), rt);
                 mRegistered = false;
             }
         }
@@ -707,7 +691,6 @@
             final Sensor sensor = mSensors[mPosture];
             mDozeLog.traceSensor(mPulseReason);
             mHandler.post(mWakeLock.wrap(() -> {
-                if (DEBUG) Log.d(TAG, "onTrigger: " + triggerEventToString(event));
                 if (sensor != null && sensor.getType() == Sensor.TYPE_PICK_UP_GESTURE) {
                     UI_EVENT_LOGGER.log(DozeSensorsUiEvent.ACTION_AMBIENT_GESTURE_PICKUP);
                 }
@@ -779,11 +762,11 @@
                     && !mRegistered) {
                 asyncSensorManager.registerPluginListener(mPluginSensor, this);
                 mRegistered = true;
-                if (DEBUG) Log.d(TAG, "registerPluginListener");
+                mDozeLog.tracePluginSensorUpdate(true /* registered */);
             } else if (mRegistered) {
                 asyncSensorManager.unregisterPluginListener(mPluginSensor, this);
                 mRegistered = false;
-                if (DEBUG) Log.d(TAG, "unregisterPluginListener");
+                mDozeLog.tracePluginSensorUpdate(false /* registered */);
             }
         }
 
@@ -816,10 +799,9 @@
             mHandler.post(mWakeLock.wrap(() -> {
                 final long now = SystemClock.uptimeMillis();
                 if (now < mDebounceFrom + mDebounce) {
-                    Log.d(TAG, "onSensorEvent dropped: " + triggerEventToString(event));
+                    mDozeLog.traceSensorEventDropped(mPulseReason, "debounce");
                     return;
                 }
-                if (DEBUG) Log.d(TAG, "onSensorEvent: " + triggerEventToString(event));
                 mSensorCallback.onSensorPulse(mPulseReason, -1, -1, event.getValues());
             }));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
index a2eb4e3..e8d7e46 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java
@@ -17,6 +17,7 @@
 package com.android.systemui.doze;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.service.dreams.DreamService;
@@ -59,6 +60,7 @@
         mPluginManager.addPluginListener(this, DozeServicePlugin.class, false /* allowMultiple */);
         DozeComponent dozeComponent = mDozeComponentBuilder.build(this);
         mDozeMachine = dozeComponent.getDozeMachine();
+        mDozeMachine.onConfigurationChanged(getResources().getConfiguration());
     }
 
     @Override
@@ -127,6 +129,12 @@
     }
 
     @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDozeMachine.onConfigurationChanged(newConfig);
+    }
+
+    @Override
     public void onRequestHideDoze() {
         if (mDozeMachine != null) {
             mDozeMachine.requestState(DozeMachine.State.DOZE);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
index 7ed4b35..e6d9865 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSuppressor.java
@@ -16,21 +16,13 @@
 
 package com.android.systemui.doze;
 
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
 
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.PowerManager;
 import android.os.UserHandle;
 import android.text.TextUtils;
 
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.doze.dagger.DozeScope;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
@@ -43,7 +35,9 @@
 /**
  * Handles suppressing doze on:
  * 1. INITIALIZED, don't allow dozing at all when:
- *      - in CAR_MODE
+ *      - in CAR_MODE, in this scenario the device is asleep and won't listen for any triggers
+ *      to wake up. In this state, no UI shows. Unlike other conditions, this suppression is only
+ *      temporary and stops when the device exits CAR_MODE
  *      - device is NOT provisioned
  *      - there's a pending authentication
  * 2. PowerSaveMode active
@@ -57,35 +51,47 @@
  */
 @DozeScope
 public class DozeSuppressor implements DozeMachine.Part {
-    private static final String TAG = "DozeSuppressor";
 
     private DozeMachine mMachine;
     private final DozeHost mDozeHost;
     private final AmbientDisplayConfiguration mConfig;
     private final DozeLog mDozeLog;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final UiModeManager mUiModeManager;
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
 
-    private boolean mBroadcastReceiverRegistered;
+    private boolean mIsCarModeEnabled = false;
 
     @Inject
     public DozeSuppressor(
             DozeHost dozeHost,
             AmbientDisplayConfiguration config,
             DozeLog dozeLog,
-            BroadcastDispatcher broadcastDispatcher,
-            UiModeManager uiModeManager,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
         mDozeHost = dozeHost;
         mConfig = config;
         mDozeLog = dozeLog;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mUiModeManager = uiModeManager;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
     }
 
     @Override
+    public void onUiModeTypeChanged(int newUiModeType) {
+        boolean isCarModeEnabled = newUiModeType == UI_MODE_TYPE_CAR;
+        if (mIsCarModeEnabled == isCarModeEnabled) {
+            return;
+        }
+        mIsCarModeEnabled = isCarModeEnabled;
+        // Do not handle the event if doze machine is not initialized yet.
+        // It will be handled upon initialization.
+        if (mMachine.isUninitializedOrFinished()) {
+            return;
+        }
+        if (mIsCarModeEnabled) {
+            handleCarModeStarted();
+        } else {
+            handleCarModeExited();
+        }
+    }
+
+    @Override
     public void setDozeMachine(DozeMachine dozeMachine) {
         mMachine = dozeMachine;
     }
@@ -94,7 +100,6 @@
     public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
         switch (newState) {
             case INITIALIZED:
-                registerBroadcastReceiver();
                 mDozeHost.addCallback(mHostCallback);
                 checkShouldImmediatelyEndDoze();
                 checkShouldImmediatelySuspendDoze();
@@ -108,14 +113,12 @@
 
     @Override
     public void destroy() {
-        unregisterBroadcastReceiver();
         mDozeHost.removeCallback(mHostCallback);
     }
 
     private void checkShouldImmediatelySuspendDoze() {
-        if (mUiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
-            mDozeLog.traceCarModeStarted();
-            mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
+        if (mIsCarModeEnabled) {
+            handleCarModeStarted();
         }
     }
 
@@ -135,7 +138,7 @@
 
     @Override
     public void dump(PrintWriter pw) {
-        pw.println(" uiMode=" + mUiModeManager.getCurrentModeType());
+        pw.println(" isCarModeEnabled=" + mIsCarModeEnabled);
         pw.println(" hasPendingAuth="
                 + mBiometricUnlockControllerLazy.get().hasPendingAuthentication());
         pw.println(" isProvisioned=" + mDozeHost.isProvisioned());
@@ -143,40 +146,18 @@
         pw.println(" aodPowerSaveActive=" + mDozeHost.isPowerSaveActive());
     }
 
-    private void registerBroadcastReceiver() {
-        if (mBroadcastReceiverRegistered) {
-            return;
-        }
-        IntentFilter filter = new IntentFilter(ACTION_ENTER_CAR_MODE);
-        filter.addAction(ACTION_EXIT_CAR_MODE);
-        mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter);
-        mBroadcastReceiverRegistered = true;
+    private void handleCarModeExited() {
+        mDozeLog.traceCarModeEnded();
+        mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
+                ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
     }
 
-    private void unregisterBroadcastReceiver() {
-        if (!mBroadcastReceiverRegistered) {
-            return;
-        }
-        mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
-        mBroadcastReceiverRegistered = false;
+    private void handleCarModeStarted() {
+        mDozeLog.traceCarModeStarted();
+        mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
     }
 
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (ACTION_ENTER_CAR_MODE.equals(action)) {
-                mDozeLog.traceCarModeStarted();
-                mMachine.requestState(DozeMachine.State.DOZE_SUSPEND_TRIGGERS);
-            } else if (ACTION_EXIT_CAR_MODE.equals(action)) {
-                mDozeLog.traceCarModeEnded();
-                mMachine.requestState(mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT)
-                        ? DozeMachine.State.DOZE_AOD : DozeMachine.State.DOZE);
-            }
-        }
-    };
-
-    private DozeHost.Callback mHostCallback = new DozeHost.Callback() {
+    private final DozeHost.Callback mHostCallback = new DozeHost.Callback() {
         @Override
         public void onPowerSaveChanged(boolean active) {
             // handles suppression changes, while DozeMachine#transitionPolicy handles gating
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index ef454ff..32cb1c0 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -198,7 +198,7 @@
         mAllowPulseTriggers = true;
         mSessionTracker = sessionTracker;
 
-        mDozeSensors = new DozeSensors(context, mSensorManager, dozeParameters,
+        mDozeSensors = new DozeSensors(mSensorManager, dozeParameters,
                 config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
                 secureSettings, authController, devicePostureController);
         mDockManager = dockManager;
@@ -536,13 +536,13 @@
             return;
         }
 
-        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse()) {
+        if (!mAllowPulseTriggers || mDozeHost.isPulsePending() || !canPulse(dozeState)) {
             if (!mAllowPulseTriggers) {
                 mDozeLog.tracePulseDropped("requestPulse - !mAllowPulseTriggers");
             } else if (mDozeHost.isPulsePending()) {
                 mDozeLog.tracePulseDropped("requestPulse - pulsePending");
-            } else if (!canPulse()) {
-                mDozeLog.tracePulseDropped("requestPulse", dozeState);
+            } else if (!canPulse(dozeState)) {
+                mDozeLog.tracePulseDropped("requestPulse - dozeState cannot pulse", dozeState);
             }
             runIfNotNull(onPulseSuppressedListener);
             return;
@@ -559,15 +559,16 @@
                 // not in pocket, continue pulsing
                 final boolean isPulsePending = mDozeHost.isPulsePending();
                 mDozeHost.setPulsePending(false);
-                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse()) {
+                if (!isPulsePending || mDozeHost.isPulsingBlocked() || !canPulse(dozeState)) {
                     if (!isPulsePending) {
                         mDozeLog.tracePulseDropped("continuePulseRequest - pulse no longer"
                                 + " pending, pulse was cancelled before it could start"
                                 + " transitioning to pulsing state.");
                     } else if (mDozeHost.isPulsingBlocked()) {
                         mDozeLog.tracePulseDropped("continuePulseRequest - pulsingBlocked");
-                    } else if (!canPulse()) {
-                        mDozeLog.tracePulseDropped("continuePulseRequest", mMachine.getState());
+                    } else if (!canPulse(dozeState)) {
+                        mDozeLog.tracePulseDropped("continuePulseRequest"
+                                + " - doze state cannot pulse", dozeState);
                     }
                     runIfNotNull(onPulseSuppressedListener);
                     return;
@@ -582,10 +583,10 @@
                 .ifPresent(uiEventEnum -> mUiEventLogger.log(uiEventEnum, getKeyguardSessionId()));
     }
 
-    private boolean canPulse() {
-        return mMachine.getState() == DozeMachine.State.DOZE
-                || mMachine.getState() == DozeMachine.State.DOZE_AOD
-                || mMachine.getState() == DozeMachine.State.DOZE_AOD_DOCKED;
+    private boolean canPulse(DozeMachine.State dozeState) {
+        return dozeState == DozeMachine.State.DOZE
+                || dozeState == DozeMachine.State.DOZE_AOD
+                || dozeState == DozeMachine.State.DOZE_AOD_DOCKED;
     }
 
     @Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index c07d402..1166c2f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
@@ -28,7 +28,7 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaCarouselController;
+import com.android.systemui.media.controls.ui.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
index 478f861..609bd76 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpHandler.kt
@@ -24,8 +24,13 @@
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_CRITICAL
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_HIGH
 import com.android.systemui.dump.DumpHandler.Companion.PRIORITY_ARG_NORMAL
+import com.android.systemui.dump.nano.SystemUIProtoDump
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
+import com.google.protobuf.nano.MessageNano
+import java.io.BufferedOutputStream
+import java.io.FileDescriptor
+import java.io.FileOutputStream
 import java.io.PrintWriter
 import javax.inject.Inject
 import javax.inject.Provider
@@ -100,7 +105,7 @@
     /**
      * Dump the diagnostics! Behavior can be controlled via [args].
      */
-    fun dump(pw: PrintWriter, args: Array<String>) {
+    fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) {
         Trace.beginSection("DumpManager#dump()")
         val start = SystemClock.uptimeMillis()
 
@@ -111,10 +116,12 @@
             return
         }
 
-        when (parsedArgs.dumpPriority) {
-            PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
-            PRIORITY_ARG_NORMAL -> dumpNormal(pw, parsedArgs)
-            else -> dumpParameterized(pw, parsedArgs)
+        when {
+            parsedArgs.dumpPriority == PRIORITY_ARG_CRITICAL -> dumpCritical(pw, parsedArgs)
+            parsedArgs.dumpPriority == PRIORITY_ARG_NORMAL && !parsedArgs.proto -> {
+                dumpNormal(pw, parsedArgs)
+            }
+            else -> dumpParameterized(fd, pw, parsedArgs)
         }
 
         pw.println()
@@ -122,7 +129,7 @@
         Trace.endSection()
     }
 
-    private fun dumpParameterized(pw: PrintWriter, args: ParsedArgs) {
+    private fun dumpParameterized(fd: FileDescriptor, pw: PrintWriter, args: ParsedArgs) {
         when (args.command) {
             "bugreport-critical" -> dumpCritical(pw, args)
             "bugreport-normal" -> dumpNormal(pw, args)
@@ -130,7 +137,13 @@
             "buffers" -> dumpBuffers(pw, args)
             "config" -> dumpConfig(pw)
             "help" -> dumpHelp(pw)
-            else -> dumpTargets(args.nonFlagArgs, pw, args)
+            else -> {
+                if (args.proto) {
+                    dumpProtoTargets(args.nonFlagArgs, fd, args)
+                } else {
+                    dumpTargets(args.nonFlagArgs, pw, args)
+                }
+            }
         }
     }
 
@@ -160,6 +173,26 @@
         }
     }
 
+    private fun dumpProtoTargets(
+            targets: List<String>,
+            fd: FileDescriptor,
+            args: ParsedArgs
+    ) {
+        val systemUIProto = SystemUIProtoDump()
+        if (targets.isNotEmpty()) {
+            for (target in targets) {
+                dumpManager.dumpProtoTarget(target, systemUIProto, args.rawArgs)
+            }
+        } else {
+            dumpManager.dumpProtoDumpables(systemUIProto, args.rawArgs)
+        }
+        val buffer = BufferedOutputStream(FileOutputStream(fd))
+        buffer.use {
+            it.write(MessageNano.toByteArray(systemUIProto))
+            it.flush()
+        }
+    }
+
     private fun dumpTargets(
         targets: List<String>,
         pw: PrintWriter,
@@ -267,6 +300,7 @@
                             }
                         }
                     }
+                    PROTO -> pArgs.proto = true
                     "-t", "--tail" -> {
                         pArgs.tailLength = readArgument(iterator, arg) {
                             it.toInt()
@@ -278,6 +312,9 @@
                     "-h", "--help" -> {
                         pArgs.command = "help"
                     }
+                    // This flag is passed as part of the proto dump in Bug reports, we can ignore
+                    // it because this is our default behavior.
+                    "-a" -> {}
                     else -> {
                         throw ArgParseException("Unknown flag: $arg")
                     }
@@ -314,7 +351,7 @@
         const val PRIORITY_ARG_CRITICAL = "CRITICAL"
         const val PRIORITY_ARG_HIGH = "HIGH"
         const val PRIORITY_ARG_NORMAL = "NORMAL"
-        const val PROTO = "--sysui_proto"
+        const val PROTO = "--proto"
     }
 }
 
@@ -338,6 +375,7 @@
     var tailLength: Int = 0
     var command: String? = null
     var listOnly = false
+    var proto = false
 }
 
 class ArgParseException(message: String) : Exception(message)
diff --git a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
index dbca651..ae78089 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/dump/DumpManager.kt
@@ -18,6 +18,8 @@
 
 import android.util.ArrayMap
 import com.android.systemui.Dumpable
+import com.android.systemui.ProtoDumpable
+import com.android.systemui.dump.nano.SystemUIProtoDump
 import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -90,7 +92,7 @@
         target: String,
         pw: PrintWriter,
         args: Array<String>,
-        tailLength: Int
+        tailLength: Int,
     ) {
         for (dumpable in dumpables.values) {
             if (dumpable.name.endsWith(target)) {
@@ -107,6 +109,36 @@
         }
     }
 
+    @Synchronized
+    fun dumpProtoTarget(
+        target: String,
+        protoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        for (dumpable in dumpables.values) {
+            if (dumpable.dumpable is ProtoDumpable && dumpable.name.endsWith(target)) {
+                dumpProtoDumpable(dumpable.dumpable, protoDump, args)
+                return
+            }
+        }
+    }
+
+    @Synchronized
+    fun dumpProtoDumpables(
+        systemUIProtoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        for (dumpable in dumpables.values) {
+            if (dumpable.dumpable is ProtoDumpable) {
+                dumpProtoDumpable(
+                    dumpable.dumpable,
+                    systemUIProtoDump,
+                    args
+                )
+            }
+        }
+    }
+
     /**
      * Dumps all registered dumpables to [pw]
      */
@@ -184,6 +216,14 @@
         buffer.dumpable.dump(pw, tailLength)
     }
 
+    private fun dumpProtoDumpable(
+        protoDumpable: ProtoDumpable,
+        systemUIProtoDump: SystemUIProtoDump,
+        args: Array<String>
+    ) {
+        protoDumpable.dumpProto(systemUIProtoDump, args)
+    }
+
     private fun canAssignToNameLocked(name: String, newDumpable: Any): Boolean {
         val existingDumpable = dumpables[name]?.dumpable ?: buffers[name]?.dumpable
         return existingDumpable == null || newDumpable == existingDumpable
@@ -195,4 +235,4 @@
     val dumpable: T
 )
 
-private const val TAG = "DumpManager"
\ No newline at end of file
+private const val TAG = "DumpManager"
diff --git a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
index 0a41a56..da983ab 100644
--- a/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
+++ b/packages/SystemUI/src/com/android/systemui/dump/SystemUIAuxiliaryDumpService.java
@@ -51,6 +51,7 @@
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         // Simulate the NORMAL priority arg being passed to us
         mDumpHandler.dump(
+                fd,
                 pw,
                 new String[] { DumpHandler.PRIORITY_ARG, DumpHandler.PRIORITY_ARG_NORMAL });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/dump/sysui.proto b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
new file mode 100644
index 0000000..cd8c08a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dump/sysui.proto
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+
+package com.android.systemui.dump;
+
+import "frameworks/base/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto";
+
+option java_multiple_files = true;
+
+message SystemUIProtoDump {
+  repeated com.android.systemui.qs.QsTileState tiles = 1;
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
deleted file mode 100644
index 5c5ed93..0000000
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ /dev/null
@@ -1,364 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.flags;
-
-import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
-
-import com.android.internal.annotations.Keep;
-import com.android.systemui.R;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * List of {@link Flag} objects for use in SystemUI.
- *
- * Flag Ids are integers.
- * Ids must be unique. This is enforced in a unit test.
- * Ids need not be sequential. Flags can "claim" a chunk of ids for flags in related features with
- * a comment. This is purely for organizational purposes.
- *
- * On public release builds, flags will always return their default value. There is no way to
- * change their value on release builds.
- *
- * See {@link FeatureFlagsDebug} for instructions on flipping the flags via adb.
- */
-public class Flags {
-    public static final UnreleasedFlag TEAMFOOD = new UnreleasedFlag(1);
-
-    /***************************************/
-    // 100 - notification
-    public static final UnreleasedFlag NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
-            new UnreleasedFlag(103);
-
-    public static final UnreleasedFlag NSSL_DEBUG_LINES =
-            new UnreleasedFlag(105);
-
-    public static final UnreleasedFlag NSSL_DEBUG_REMOVE_ANIMATION =
-            new UnreleasedFlag(106);
-
-    public static final UnreleasedFlag NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE =
-            new UnreleasedFlag(107);
-
-    public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
-            new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
-
-    public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS =
-            new ReleasedFlag(109);
-
-    public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD =
-            new UnreleasedFlag(110, true);
-
-    public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
-
-    public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
-            false);
-
-    public static final UnreleasedFlag NOTIFICATION_DISMISSAL_FADE = new UnreleasedFlag(113, true);
-
-    // next id: 114
-
-    /***************************************/
-    // 200 - keyguard/lockscreen
-
-    // ** Flag retired **
-    // public static final BooleanFlag KEYGUARD_LAYOUT =
-    //         new BooleanFlag(200, true);
-
-    public static final ReleasedFlag LOCKSCREEN_ANIMATIONS =
-            new ReleasedFlag(201);
-
-    public static final ReleasedFlag NEW_UNLOCK_SWIPE_ANIMATION =
-            new ReleasedFlag(202);
-
-    public static final ResourceBooleanFlag CHARGING_RIPPLE =
-            new ResourceBooleanFlag(203, R.bool.flag_charging_ripple);
-
-    public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER =
-            new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher);
-
-    public static final ResourceBooleanFlag FACE_SCANNING_ANIM =
-            new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation);
-
-    public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207);
-  
-    /**
-     * Flag to enable the usage of the new bouncer data source. This is a refactor of and
-     * eventual replacement of KeyguardBouncer.java.
-     */
-    public static final UnreleasedFlag MODERN_BOUNCER = new UnreleasedFlag(208);
-
-    /**
-     * Whether the user interactor and repository should use `UserSwitcherController`.
-     *
-     * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
-     * the framework APIs.
-     */
-    public static final ReleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
-            new ReleasedFlag(210);
-
-    /**
-     * Whether `UserSwitcherController` should use the user interactor.
-     *
-     * <p>When this is {@code true}, the controller does not directly access framework APIs.
-     * Instead, it goes through the interactor.
-     *
-     * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
-     * {@code true} as it would created a cycle between controller -> interactor -> controller.
-     */
-    public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = new UnreleasedFlag(211);
-
-    /**
-     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
-     * the digits when the clock moves.
-     */
-    public static final UnreleasedFlag STEP_CLOCK_ANIMATION = new UnreleasedFlag(212);
-
-    /***************************************/
-    // 300 - power menu
-    public static final ReleasedFlag POWER_MENU_LITE =
-            new ReleasedFlag(300);
-
-    /***************************************/
-    // 400 - smartspace
-    public static final ReleasedFlag SMARTSPACE_DEDUPING =
-            new ReleasedFlag(400);
-
-    public static final ReleasedFlag SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED =
-            new ReleasedFlag(401);
-
-    public static final ResourceBooleanFlag SMARTSPACE =
-            new ResourceBooleanFlag(402, R.bool.flag_smartspace);
-
-    /***************************************/
-    // 500 - quick settings
-    /**
-     * @deprecated Not needed anymore
-     */
-    @Deprecated
-    public static final ReleasedFlag NEW_USER_SWITCHER =
-            new ReleasedFlag(500);
-
-    public static final UnreleasedFlag COMBINED_QS_HEADERS =
-            new UnreleasedFlag(501, true);
-
-    public static final ResourceBooleanFlag PEOPLE_TILE =
-            new ResourceBooleanFlag(502, R.bool.flag_conversations);
-
-    public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT =
-            new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut);
-
-    /**
-     * @deprecated Not needed anymore
-     */
-    @Deprecated
-    public static final ReleasedFlag NEW_FOOTER = new ReleasedFlag(504);
-
-    public static final UnreleasedFlag NEW_HEADER = new UnreleasedFlag(505, true);
-    public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER =
-            new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher);
-
-    public static final ReleasedFlag NEW_FOOTER_ACTIONS = new ReleasedFlag(507);
-
-    /***************************************/
-    // 600- status bar
-    public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
-            new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip);
-
-    public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
-            new ReleasedFlag(603, false);
-
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_BACKEND =
-            new UnreleasedFlag(604, false);
-
-    public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE_FRONTEND =
-            new UnreleasedFlag(605, false);
-
-    /***************************************/
-    // 700 - dialer/calls
-    public static final ReleasedFlag ONGOING_CALL_STATUS_BAR_CHIP =
-            new ReleasedFlag(700);
-
-    public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE =
-            new ReleasedFlag(701);
-
-    public static final ReleasedFlag ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP =
-            new ReleasedFlag(702);
-
-    /***************************************/
-    // 800 - general visual/theme
-    public static final ResourceBooleanFlag MONET =
-            new ResourceBooleanFlag(800, R.bool.flag_monet);
-
-    /***************************************/
-    // 801 - region sampling
-    public static final UnreleasedFlag REGION_SAMPLING = new UnreleasedFlag(801);
-
-    // 802 - wallpaper rendering
-    public static final UnreleasedFlag USE_CANVAS_RENDERER = new UnreleasedFlag(802, true);
-
-    // 803 - screen contents translation
-    public static final UnreleasedFlag SCREEN_CONTENTS_TRANSLATION = new UnreleasedFlag(803);
-
-    /***************************************/
-    // 900 - media
-    public static final ReleasedFlag MEDIA_TAP_TO_TRANSFER = new ReleasedFlag(900);
-    public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
-    public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
-    public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
-    public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905);
-    public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906);
-    public static final UnreleasedFlag UMO_SURFACE_RIPPLE = new UnreleasedFlag(907);
-
-    // 1000 - dock
-    public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
-            new ReleasedFlag(1000);
-    public static final ReleasedFlag DOCK_SETUP_ENABLED = new ReleasedFlag(1001);
-
-    public static final UnreleasedFlag ROUNDED_BOX_RIPPLE =
-            new UnreleasedFlag(1002, /* teamfood= */ true);
-
-    public static final UnreleasedFlag REFACTORED_DOCK_SETUP = new UnreleasedFlag(1003, true);
-
-    // 1100 - windowing
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_SHELL_TRANSITIONS =
-            new SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false);
-
-    /**
-     * b/170163464: animate bubbles expanded view collapse with home gesture
-     */
-    @Keep
-    public static final SysPropBooleanFlag BUBBLES_HOME_GESTURE =
-            new SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true);
-
-    @Keep
-    public static final DeviceConfigBooleanFlag WM_ENABLE_PARTIAL_SCREEN_SHARING =
-            new DeviceConfigBooleanFlag(1102, "record_task_content",
-                    NAMESPACE_WINDOW_MANAGER, false, true);
-
-    @Keep
-    public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW =
-            new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false);
-
-    @Keep
-    public static final SysPropBooleanFlag WM_DESKTOP_WINDOWING =
-            new SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false);
-
-    @Keep
-    public static final SysPropBooleanFlag WM_CAPTION_ON_SHELL =
-            new SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false);
-
-    @Keep
-    public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
-            new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
-
-    @Keep
-    public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
-            new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
-
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
-            new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
-            new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
-
-    @Keep
-    public static final SysPropBooleanFlag ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
-            new SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false);
-
-    // 1200 - predictive back
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
-            1200, "persist.wm.debug.predictive_back", true);
-    @Keep
-    public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK_ANIM = new SysPropBooleanFlag(
-            1201, "persist.wm.debug.predictive_back_anim", false);
-    @Keep
-    public static final SysPropBooleanFlag WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
-            new SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false);
-
-    public static final UnreleasedFlag NEW_BACK_AFFORDANCE =
-            new UnreleasedFlag(1203, false /* teamfood */);
-
-    // 1300 - screenshots
-
-    public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
-    public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
-
-    // 1400 - columbus
-    public static final ReleasedFlag QUICK_TAP_IN_PCC = new ReleasedFlag(1400);
-
-    // 1500 - chooser
-    public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500);
-
-    // 1600 - accessibility
-    public static final UnreleasedFlag A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
-            new UnreleasedFlag(1600);
-
-    // 1700 - clipboard
-    public static final UnreleasedFlag CLIPBOARD_OVERLAY_REFACTOR = new UnreleasedFlag(1700);
-
-    // Pay no attention to the reflection behind the curtain.
-    // ========================== Curtain ==========================
-    // |                                                           |
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    private static Map<Integer, Flag<?>> sFlagMap;
-    static Map<Integer, Flag<?>> collectFlags() {
-        if (sFlagMap != null) {
-            return sFlagMap;
-        }
-
-        Map<Integer, Flag<?>> flags = new HashMap<>();
-        List<Field> flagFields = getFlagFields();
-
-        for (Field field : flagFields) {
-            try {
-                Flag<?> flag = (Flag<?>) field.get(null);
-                flags.put(flag.getId(), flag);
-            } catch (IllegalAccessException e) {
-                // no-op
-            }
-        }
-
-        sFlagMap = flags;
-
-        return sFlagMap;
-    }
-
-    static List<Field> getFlagFields() {
-        Field[] fields = Flags.class.getFields();
-        List<Field> result = new ArrayList<>();
-
-        for (Field field : fields) {
-            Class<?> t = field.getType();
-            if (Flag.class.isAssignableFrom(t)) {
-                result.add(field);
-            }
-        }
-
-        return result;
-    }
-    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
-    // |                                                           |
-    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
-
-}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
new file mode 100644
index 0000000..9ca6dd6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.flags
+
+import android.provider.DeviceConfig
+import com.android.internal.annotations.Keep
+import com.android.systemui.R
+import java.lang.reflect.Field
+
+/**
+ * List of [Flag] objects for use in SystemUI.
+ *
+ * Flag Ids are integers. Ids must be unique. This is enforced in a unit test. Ids need not be
+ * sequential. Flags can "claim" a chunk of ids for flags in related features with a comment. This
+ * is purely for organizational purposes.
+ *
+ * On public release builds, flags will always return their default value. There is no way to change
+ * their value on release builds.
+ *
+ * See [FeatureFlagsDebug] for instructions on flipping the flags via adb.
+ */
+object Flags {
+    @JvmField val TEAMFOOD = UnreleasedFlag(1)
+
+    // 100 - notification
+    // TODO(b/254512751): Tracking Bug
+    val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = UnreleasedFlag(103)
+
+    // TODO(b/254512732): Tracking Bug
+    @JvmField val NSSL_DEBUG_LINES = UnreleasedFlag(105)
+
+    // TODO(b/254512505): Tracking Bug
+    @JvmField val NSSL_DEBUG_REMOVE_ANIMATION = UnreleasedFlag(106)
+
+    // TODO(b/254512624): Tracking Bug
+    @JvmField
+    val NOTIFICATION_DRAG_TO_CONTENTS =
+        ResourceBooleanFlag(108, R.bool.config_notificationToContents)
+
+    // TODO(b/254512517): Tracking Bug
+    val FSI_REQUIRES_KEYGUARD = UnreleasedFlag(110, teamfood = true)
+
+    // TODO(b/254512538): Tracking Bug
+    val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true)
+
+    // TODO(b/254512425): Tracking Bug
+    val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = false)
+
+    // TODO(b/254512731): Tracking Bug
+    @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
+    val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true)
+    val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true)
+    @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true)
+    // next id: 117
+
+    // 200 - keyguard/lockscreen
+    // ** Flag retired **
+    // public static final BooleanFlag KEYGUARD_LAYOUT =
+    //         new BooleanFlag(200, true);
+    // TODO(b/254512713): Tracking Bug
+    @JvmField val LOCKSCREEN_ANIMATIONS = ReleasedFlag(201)
+
+    // TODO(b/254512750): Tracking Bug
+    val NEW_UNLOCK_SWIPE_ANIMATION = ReleasedFlag(202)
+    val CHARGING_RIPPLE = ResourceBooleanFlag(203, R.bool.flag_charging_ripple)
+
+    // TODO(b/254512281): Tracking Bug
+    @JvmField
+    val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher)
+
+    // TODO(b/254512676): Tracking Bug
+    @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true)
+
+    /**
+     * Flag to enable the usage of the new bouncer data source. This is a refactor of and eventual
+     * replacement of KeyguardBouncer.java.
+     */
+    // TODO(b/254512385): Tracking Bug
+    @JvmField val MODERN_BOUNCER = UnreleasedFlag(208)
+
+    /**
+     * Whether the user interactor and repository should use `UserSwitcherController`.
+     *
+     * If this is `false`, the interactor and repo skip the controller and directly access the
+     * framework APIs.
+     */
+    // TODO(b/254513286): Tracking Bug
+    val USER_INTERACTOR_AND_REPO_USE_CONTROLLER = UnreleasedFlag(210)
+
+    /**
+     * Whether `UserSwitcherController` should use the user interactor.
+     *
+     * When this is `true`, the controller does not directly access framework APIs. Instead, it goes
+     * through the interactor.
+     *
+     * Note: do not set this to true if [.USER_INTERACTOR_AND_REPO_USE_CONTROLLER] is `true` as it
+     * would created a cycle between controller -> interactor -> controller.
+     */
+    // TODO(b/254513102): Tracking Bug
+    val USER_CONTROLLER_USES_INTERACTOR = ReleasedFlag(211)
+
+    /**
+     * Whether the clock on a wide lock screen should use the new "stepping" animation for moving
+     * the digits when the clock moves.
+     */
+    @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212)
+
+    // 300 - power menu
+    // TODO(b/254512600): Tracking Bug
+    @JvmField val POWER_MENU_LITE = ReleasedFlag(300)
+
+    // 400 - smartspace
+
+    // TODO(b/254513100): Tracking Bug
+    val SMARTSPACE_SHARED_ELEMENT_TRANSITION_ENABLED = ReleasedFlag(401)
+    val SMARTSPACE = ResourceBooleanFlag(402, R.bool.flag_smartspace)
+
+    // 500 - quick settings
+    @Deprecated("Not needed anymore") val NEW_USER_SWITCHER = ReleasedFlag(500)
+
+    // TODO(b/254512321): Tracking Bug
+    @JvmField val COMBINED_QS_HEADERS = ReleasedFlag(501, teamfood = true)
+    val PEOPLE_TILE = ResourceBooleanFlag(502, R.bool.flag_conversations)
+    @JvmField
+    val QS_USER_DETAIL_SHORTCUT =
+        ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut)
+
+    // TODO(b/254512699): Tracking Bug
+    @Deprecated("Not needed anymore") val NEW_FOOTER = ReleasedFlag(504)
+
+    // TODO(b/254512747): Tracking Bug
+    val NEW_HEADER = ReleasedFlag(505, teamfood = true)
+
+    // TODO(b/254512383): Tracking Bug
+    @JvmField
+    val FULL_SCREEN_USER_SWITCHER =
+        ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher)
+
+    // TODO(b/254512678): Tracking Bug
+    @JvmField val NEW_FOOTER_ACTIONS = ReleasedFlag(507)
+
+    // 600- status bar
+    // TODO(b/254513246): Tracking Bug
+    val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip)
+
+    // TODO(b/254513025): Tracking Bug
+    val STATUS_BAR_LETTERBOX_APPEARANCE = ReleasedFlag(603, teamfood = false)
+
+    // TODO(b/254512623): Tracking Bug
+    @Deprecated("Replaced by mobile and wifi specific flags.")
+    val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false)
+
+    // TODO(b/254512660): Tracking Bug
+    @Deprecated("Replaced by mobile and wifi specific flags.")
+    val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false)
+
+    val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606, false)
+
+    val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607, false)
+
+    // 700 - dialer/calls
+    // TODO(b/254512734): Tracking Bug
+    val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700)
+
+    // TODO(b/254512681): Tracking Bug
+    val ONGOING_CALL_IN_IMMERSIVE = ReleasedFlag(701)
+
+    // TODO(b/254512753): Tracking Bug
+    val ONGOING_CALL_IN_IMMERSIVE_CHIP_TAP = ReleasedFlag(702)
+
+    // 800 - general visual/theme
+    @JvmField val MONET = ResourceBooleanFlag(800, R.bool.flag_monet)
+
+    // 801 - region sampling
+    // TODO(b/254512848): Tracking Bug
+    val REGION_SAMPLING = UnreleasedFlag(801)
+
+    // 802 - wallpaper rendering
+    // TODO(b/254512923): Tracking Bug
+    @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802)
+
+    // 803 - screen contents translation
+    // TODO(b/254513187): Tracking Bug
+    val SCREEN_CONTENTS_TRANSLATION = UnreleasedFlag(803)
+
+    // 804 - monochromatic themes
+    @JvmField val MONOCHROMATIC_THEMES = UnreleasedFlag(804)
+
+    // 900 - media
+    // TODO(b/254512697): Tracking Bug
+    val MEDIA_TAP_TO_TRANSFER = ReleasedFlag(900)
+
+    // TODO(b/254512502): Tracking Bug
+    val MEDIA_SESSION_ACTIONS = UnreleasedFlag(901)
+
+    // TODO(b/254512726): Tracking Bug
+    val MEDIA_NEARBY_DEVICES = ReleasedFlag(903)
+
+    // TODO(b/254512695): Tracking Bug
+    val MEDIA_MUTE_AWAIT = ReleasedFlag(904)
+
+    // TODO(b/254512654): Tracking Bug
+    @JvmField val DREAM_MEDIA_COMPLICATION = UnreleasedFlag(905)
+
+    // TODO(b/254512673): Tracking Bug
+    @JvmField val DREAM_MEDIA_TAP_TO_OPEN = UnreleasedFlag(906)
+
+    // TODO(b/254513168): Tracking Bug
+    val UMO_SURFACE_RIPPLE = UnreleasedFlag(907)
+
+    // 1000 - dock
+    val SIMULATE_DOCK_THROUGH_CHARGING = ReleasedFlag(1000)
+
+    // TODO(b/254512444): Tracking Bug
+    @JvmField val DOCK_SETUP_ENABLED = ReleasedFlag(1001)
+
+    // TODO(b/254512758): Tracking Bug
+    @JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002)
+
+    // TODO(b/254512525): Tracking Bug
+    @JvmField val REFACTORED_DOCK_SETUP = ReleasedFlag(1003, teamfood = true)
+
+    // 1100 - windowing
+    @Keep
+    val WM_ENABLE_SHELL_TRANSITIONS =
+        SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false)
+
+    /** b/170163464: animate bubbles expanded view collapse with home gesture */
+    @Keep
+    val BUBBLES_HOME_GESTURE =
+        SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true)
+
+    // TODO(b/254513207): Tracking Bug
+    @JvmField
+    @Keep
+    val WM_ENABLE_PARTIAL_SCREEN_SHARING =
+        DeviceConfigBooleanFlag(
+            1102,
+            "record_task_content",
+            DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+            false,
+            teamfood = true
+        )
+
+    // TODO(b/254512674): Tracking Bug
+    @JvmField
+    @Keep
+    val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false)
+
+    @Keep
+    val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false)
+
+    @Keep
+    val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false)
+
+    @Keep
+    val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false)
+
+    @Keep
+    val SHOW_FLOATING_TASKS_AS_BUBBLES =
+        SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false)
+
+    @Keep
+    val ENABLE_FLING_TO_DISMISS_BUBBLE =
+        SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true)
+
+    @Keep
+    val ENABLE_FLING_TO_DISMISS_PIP =
+        SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true)
+
+    @Keep
+    val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
+        SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false)
+
+    // 1200 - predictive back
+    @Keep
+    val WM_ENABLE_PREDICTIVE_BACK =
+        SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true)
+
+    @Keep
+    val WM_ENABLE_PREDICTIVE_BACK_ANIM =
+        SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false)
+
+    @Keep
+    val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
+        SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false)
+
+    // TODO(b/254512728): Tracking Bug
+    @JvmField val NEW_BACK_AFFORDANCE = UnreleasedFlag(1203, teamfood = false)
+
+    // 1300 - screenshots
+    // TODO(b/254512719): Tracking Bug
+    @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300)
+
+    // TODO(b/254513155): Tracking Bug
+    @JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301)
+
+    // 1400 - columbus
+    // TODO(b/254512756): Tracking Bug
+    val QUICK_TAP_IN_PCC = ReleasedFlag(1400)
+
+    // 1500 - chooser
+    // TODO(b/254512507): Tracking Bug
+    val CHOOSER_UNBUNDLED = UnreleasedFlag(1500)
+
+    // 1600 - accessibility
+    @JvmField val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS = UnreleasedFlag(1600)
+
+    // 1700 - clipboard
+    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700)
+
+    // 1800 - shade container
+    @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true)
+
+    // Pay no attention to the reflection behind the curtain.
+    // ========================== Curtain ==========================
+    // |                                                           |
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    @JvmStatic
+    fun collectFlags(): Map<Int, Flag<*>> {
+        return flagFields
+            .map { field ->
+                // field[null] returns the current value of the field.
+                // See java.lang.Field#get
+                val flag = field[null] as Flag<*>
+                flag.id to flag
+            }
+            .toMap()
+    }
+
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    @JvmStatic
+    val flagFields: List<Field>
+        get() {
+            return Flags::class.java.fields.filter { f ->
+                Flag::class.java.isAssignableFrom(f.type)
+            }
+        }
+    // |                                                           |
+    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
+}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index da5819a..3ef5499 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -116,6 +116,7 @@
 import com.android.systemui.MultiListLayout.MultiListAdapter;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
@@ -448,10 +449,11 @@
      *
      * @param keyguardShowing     True if keyguard is showing
      * @param isDeviceProvisioned True if device is provisioned
-     * @param view                The view from which we should animate the dialog when showing it
+     * @param expandable          The expandable from which we should animate the dialog when
+     *                            showing it
      */
     public void showOrHideDialog(boolean keyguardShowing, boolean isDeviceProvisioned,
-            @Nullable View view) {
+            @Nullable Expandable expandable) {
         mKeyguardShowing = keyguardShowing;
         mDeviceProvisioned = isDeviceProvisioned;
         if (mDialog != null && mDialog.isShowing()) {
@@ -463,7 +465,7 @@
             mDialog.dismiss();
             mDialog = null;
         } else {
-            handleShow(view);
+            handleShow(expandable);
         }
     }
 
@@ -495,7 +497,7 @@
         }
     }
 
-    protected void handleShow(@Nullable View view) {
+    protected void handleShow(@Nullable Expandable expandable) {
         awakenIfNecessary();
         mDialog = createDialog();
         prepareDialog();
@@ -507,10 +509,12 @@
         // Don't acquire soft keyboard focus, to avoid destroying state when capturing bugreports
         mDialog.getWindow().addFlags(FLAG_ALT_FOCUSABLE_IM);
 
-        if (view != null) {
-            mDialogLaunchAnimator.showFromView(mDialog, view,
-                    new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                            INTERACTION_JANK_TAG));
+        DialogLaunchAnimator.Controller controller =
+                expandable != null ? expandable.dialogLaunchController(
+                        new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG)) : null;
+        if (controller != null) {
+            mDialogLaunchAnimator.show(mDialog, controller);
         } else {
             mDialog.show();
         }
@@ -1016,8 +1020,9 @@
                             Log.w(TAG, "Bugreport handler could not be launched");
                             mIActivityManager.requestInteractiveBugReport();
                         }
-                        // Close shade so user sees the activity
-                        mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                        // Maybe close shade (depends on a flag) so user sees the activity
+                        mCentralSurfacesOptional.ifPresent(
+                                CentralSurfaces::collapseShadeForBugreport);
                     } catch (RemoteException e) {
                     }
                 }
@@ -1036,8 +1041,8 @@
                 mMetricsLogger.action(MetricsEvent.ACTION_BUGREPORT_FROM_POWER_MENU_FULL);
                 mUiEventLogger.log(GlobalActionsEvent.GA_BUGREPORT_LONG_PRESS);
                 mIActivityManager.requestFullBugReport();
-                // Close shade so user sees the activity
-                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShade);
+                // Maybe close shade (depends on a flag) so user sees the activity
+                mCentralSurfacesOptional.ifPresent(CentralSurfaces::collapseShadeForBugreport);
             } catch (RemoteException e) {
             }
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 84bd8ce..0d74dc8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -401,6 +401,11 @@
     private final float mWindowCornerRadius;
 
     /**
+     * The duration in milliseconds of the dream open animation.
+     */
+    private final int mDreamOpenAnimationDuration;
+
+    /**
      * The animation used for hiding keyguard. This is used to fetch the animation timings if
      * WindowManager is not providing us with them.
      */
@@ -751,6 +756,7 @@
             if (DEBUG) Log.d(TAG, "keyguardGone");
             mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
             mKeyguardDisplayManager.hide();
+            mUpdateMonitor.startBiometricWatchdog();
             Trace.endSection();
         }
 
@@ -946,8 +952,7 @@
                         }
 
                         mOccludeByDreamAnimator = ValueAnimator.ofFloat(0f, 1f);
-                        // Use the same duration as for the UNOCCLUDE.
-                        mOccludeByDreamAnimator.setDuration(UNOCCLUDE_ANIMATION_DURATION);
+                        mOccludeByDreamAnimator.setDuration(mDreamOpenAnimationDuration);
                         mOccludeByDreamAnimator.setInterpolator(Interpolators.LINEAR);
                         mOccludeByDreamAnimator.addUpdateListener(
                                 animation -> {
@@ -1179,6 +1184,9 @@
         mPowerButtonY = context.getResources().getDimensionPixelSize(
                 R.dimen.physical_power_button_center_screen_location_y);
         mWindowCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
+
+        mDreamOpenAnimationDuration = context.getResources().getInteger(
+                com.android.internal.R.integer.config_dreamOpenAnimationDuration);
     }
 
     public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 56f1ac4..56a1f1a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -43,6 +43,7 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
+import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -72,6 +73,7 @@
             FalsingModule.class,
             KeyguardQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
+            StartKeyguardTransitionModule.class,
         })
 public class KeyguardModule {
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 45b668e..b186ae0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.doze.DozeHost
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
@@ -85,6 +86,9 @@
      */
     val dozeAmount: Flow<Float>
 
+    /** Observable for the [StatusBarState] */
+    val statusBarState: Flow<StatusBarState>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -185,6 +189,24 @@
         return keyguardStateController.isShowing
     }
 
+    override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow {
+        val callback =
+            object : StatusBarStateController.StateListener {
+                override fun onStateChanged(state: Int) {
+                    trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state")
+                }
+            }
+
+        statusBarStateController.addCallback(callback)
+        trySendWithFailureLogging(
+            statusBarStateIntToObject(statusBarStateController.getState()),
+            TAG,
+            "initial state"
+        )
+
+        awaitClose { statusBarStateController.removeCallback(callback) }
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.value = animate
     }
@@ -197,6 +219,15 @@
         _clockPosition.value = Position(x, y)
     }
 
+    private fun statusBarStateIntToObject(value: Int): StatusBarState {
+        return when (value) {
+            0 -> StatusBarState.SHADE
+            1 -> StatusBarState.KEYGUARD
+            2 -> StatusBarState.SHADE_LOCKED
+            else -> throw IllegalArgumentException("Invalid StatusBarState value: $value")
+        }
+    }
+
     companion object {
         private const val TAG = "KeyguardRepositoryImpl"
     }
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
new file mode 100644
index 0000000..e8532ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.data.repository
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.annotation.FloatRange
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filter
+
+@SysUISingleton
+class KeyguardTransitionRepository @Inject constructor() {
+    /*
+     * Each transition between [KeyguardState]s will have an associated Flow.
+     * In order to collect these events, clients should call [transition].
+     */
+    private val _transitions = MutableStateFlow(TransitionStep())
+    val transitions = _transitions.asStateFlow()
+
+    /* Information about the active transition. */
+    private var currentTransitionInfo: TransitionInfo? = null
+    /*
+     * When manual control of the transition is requested, a unique [UUID] is used as the handle
+     * to permit calls to [updateTransition]
+     */
+    private var updateTransitionId: UUID? = null
+
+    /**
+     * Interactors that require information about changes between [KeyguardState]s will call this to
+     * register themselves for flowable [TransitionStep]s when that transition occurs.
+     */
+    fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
+        return transitions.filter { step -> step.from == from && step.to == to }
+    }
+
+    /**
+     * Begin a transition from one state to another. The [info.from] must match
+     * [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid
+     * unplanned transitions.
+     */
+    fun startTransition(info: TransitionInfo): UUID? {
+        if (currentTransitionInfo != null) {
+            // Open questions:
+            // * Queue of transitions? buffer of 1?
+            // * Are transitions cancellable if a new one is triggered?
+            // * What validation does this need to do?
+            Log.wtf(TAG, "Transition still active: $currentTransitionInfo")
+            return null
+        }
+        currentTransitionInfo?.animator?.cancel()
+
+        currentTransitionInfo = info
+        info.animator?.let { animator ->
+            // An animator was provided, so use it to run the transition
+            animator.setFloatValues(0f, 1f)
+            val updateListener =
+                object : AnimatorUpdateListener {
+                    override fun onAnimationUpdate(animation: ValueAnimator) {
+                        emitTransition(
+                            info,
+                            (animation.getAnimatedValue() as Float),
+                            TransitionState.RUNNING
+                        )
+                    }
+                }
+            val adapter =
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator) {
+                        Log.i(TAG, "Starting transition: $info")
+                        emitTransition(info, 0f, TransitionState.STARTED)
+                    }
+                    override fun onAnimationCancel(animation: Animator) {
+                        Log.i(TAG, "Cancelling transition: $info")
+                    }
+                    override fun onAnimationEnd(animation: Animator) {
+                        Log.i(TAG, "Ending transition: $info")
+                        emitTransition(info, 1f, TransitionState.FINISHED)
+                        animator.removeListener(this)
+                        animator.removeUpdateListener(updateListener)
+                    }
+                }
+            animator.addListener(adapter)
+            animator.addUpdateListener(updateListener)
+            animator.start()
+            return@startTransition null
+        }
+            ?: run {
+                Log.i(TAG, "Starting transition (manual): $info")
+                emitTransition(info, 0f, TransitionState.STARTED)
+
+                // No animator, so it's manual. Provide a mechanism to callback
+                updateTransitionId = UUID.randomUUID()
+                return@startTransition updateTransitionId
+            }
+    }
+
+    /**
+     * Allows manual control of a transition. When calling [startTransition], the consumer must pass
+     * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
+     * further updates.
+     *
+     * When the transition is over, TransitionState.FINISHED must be passed into the [state]
+     * parameter.
+     */
+    fun updateTransition(
+        transitionId: UUID,
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        state: TransitionState
+    ) {
+        if (updateTransitionId != transitionId) {
+            Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
+            return
+        }
+
+        if (currentTransitionInfo == null) {
+            Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'")
+            return
+        }
+
+        currentTransitionInfo?.let { info ->
+            if (state == TransitionState.FINISHED) {
+                updateTransitionId = null
+                Log.i(TAG, "Ending transition: $info")
+            }
+
+            emitTransition(info, value, state)
+        }
+    }
+
+    private fun emitTransition(
+        info: TransitionInfo,
+        value: Float,
+        transitionState: TransitionState
+    ) {
+        if (transitionState == TransitionState.FINISHED) {
+            currentTransitionInfo = null
+        }
+        _transitions.value = TransitionStep(info.from, info.to, value, transitionState)
+    }
+
+    companion object {
+        private const val TAG = "KeyguardTransitionRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..4003766
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class AodLockscreenTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("AOD<->LOCKSCREEN") {
+
+    override fun start() {
+        scope.launch {
+            keyguardRepository.isDozing.collect { isDozing ->
+                if (isDozing) {
+                    keyguardTransitionRepository.startTransition(
+                        TransitionInfo(
+                            name,
+                            KeyguardState.LOCKSCREEN,
+                            KeyguardState.AOD,
+                            getAnimator(),
+                        )
+                    )
+                } else {
+                    keyguardTransitionRepository.startTransition(
+                        TransitionInfo(
+                            name,
+                            KeyguardState.AOD,
+                            KeyguardState.LOCKSCREEN,
+                            getAnimator(),
+                        )
+                    )
+                }
+            }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
index 7d4db37..2af9318 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
@@ -273,8 +273,8 @@
     /** Tell the bouncer to start the pre hide animation. */
     fun startDisappearAnimation(runnable: Runnable) {
         val finishRunnable = Runnable {
-            repository.setStartDisappearAnimation(null)
             runnable.run()
+            repository.setStartDisappearAnimation(null)
         }
         repository.setStartDisappearAnimation(finishRunnable)
     }
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 192919e..fc2269c 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
@@ -38,7 +38,7 @@
     val dozeAmount: Flow<Float> = repository.dozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
-    /** Whether the keyguard is showing ot not. */
+    /** Whether the keyguard is showing to not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
 
     fun isKeyguardShowing(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
new file mode 100644
index 0000000..b166681
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.domain.interactor
+
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import java.util.Set
+import javax.inject.Inject
+
+@SysUISingleton
+class KeyguardTransitionCoreStartable
+@Inject
+constructor(
+    private val interactors: Set<TransitionInteractor>,
+) : CoreStartable {
+
+    override fun start() {
+        // By listing the interactors in a when, the compiler will help enforce all classes
+        // extending the sealed class [TransitionInteractor] will be initialized.
+        interactors.forEach {
+            // `when` needs to be an expression in order for the compiler to enforce it being
+            // exhaustive
+            val ret =
+                when (it) {
+                    is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+                }
+            it.start()
+        }
+    }
+
+    companion object {
+        private const val TAG = "KeyguardTransitionCoreStartable"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
new file mode 100644
index 0000000..59bb22786
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -0,0 +1,37 @@
+/*
+ *  Copyright (C) 2022 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Encapsulates business-logic related to the keyguard transitions. */
+@SysUISingleton
+class KeyguardTransitionInteractor
+@Inject
+constructor(
+    repository: KeyguardTransitionRepository,
+) {
+    /** AOD->LOCKSCREEN transition information. */
+    val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
new file mode 100644
index 0000000..3c2a12e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class LockscreenBouncerTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardRepository: KeyguardRepository,
+    private val shadeRepository: ShadeRepository,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor("LOCKSCREEN<->BOUNCER") {
+
+    private var transitionId: UUID? = null
+
+    override fun start() {
+        scope.launch {
+            shadeRepository.shadeModel.sample(
+                combine(
+                    keyguardTransitionRepository.transitions,
+                    keyguardRepository.statusBarState,
+                ) { transitions, statusBarState ->
+                    Pair(transitions, statusBarState)
+                }
+            ) { shadeModel, pair ->
+                val (transitions, statusBarState) = pair
+
+                val id = transitionId
+                if (id != null) {
+                    // An existing `id` means a transition is started, and calls to
+                    // `updateTransition` will control it until FINISHED
+                    keyguardTransitionRepository.updateTransition(
+                        id,
+                        shadeModel.expansionAmount,
+                        if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) {
+                            transitionId = null
+                            TransitionState.FINISHED
+                        } else {
+                            TransitionState.RUNNING
+                        }
+                    )
+                } else {
+                    // TODO (b/251849525): Remove statusbarstate check when that state is integrated
+                    // into KeyguardTransitionRepository
+                    val isOnLockscreen =
+                        transitions.transitionState == TransitionState.FINISHED &&
+                            transitions.to == KeyguardState.LOCKSCREEN
+                    if (
+                        isOnLockscreen &&
+                            shadeModel.isUserDragging &&
+                            statusBarState != SHADE_LOCKED
+                    ) {
+                        transitionId =
+                            keyguardTransitionRepository.startTransition(
+                                TransitionInfo(
+                                    ownerName = name,
+                                    from = KeyguardState.LOCKSCREEN,
+                                    to = KeyguardState.BOUNCER,
+                                    animator = null,
+                                )
+                            )
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
new file mode 100644
index 0000000..74c542c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+
+@Module
+abstract class StartKeyguardTransitionModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(KeyguardTransitionCoreStartable::class)
+    abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable
+
+    @Binds
+    @IntoSet
+    abstract fun lockscreenBouncer(
+        impl: LockscreenBouncerTransitionInteractor
+    ): TransitionInteractor
+
+    @Binds
+    @IntoSet
+    abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
new file mode 100644
index 0000000..a2a46d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+/**
+ * Each TransitionInteractor is responsible for determining under which conditions to notify
+ * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is
+ * determined by [KeyguardTransitionRepository].
+ *
+ * [name] field should be a unique identifiable string representing this state, used primarily for
+ * logging
+ *
+ * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the
+ * 'when' clause of [KeyguardTransitionCoreStartable]
+ */
+sealed class TransitionInteractor(val name: String) {
+
+    abstract fun start()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
new file mode 100644
index 0000000..f66d5d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** List of all possible states to transition to/from */
+enum class KeyguardState {
+    /** For initialization only */
+    NONE,
+    /* Always-on Display. The device is in a low-power mode with a minimal UI visible */
+    AOD,
+    /*
+     * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS
+     * (Fingerprint Sensor) variations, for the user to verify their credentials
+     */
+    BOUNCER,
+    /*
+     * Device is actively displaying keyguard UI and is not in low-power mode. Device may be
+     * unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
+     */
+    LOCKSCREEN,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
new file mode 100644
index 0000000..bb95347
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** See [com.android.systemui.statusbar.StatusBarState] for definitions */
+enum class StatusBarState {
+    SHADE,
+    KEYGUARD,
+    SHADE_LOCKED,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
new file mode 100644
index 0000000..bfccf3fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+import android.animation.ValueAnimator
+
+/** Tracks who is controlling the current transition, and how to run it. */
+data class TransitionInfo(
+    val ownerName: String,
+    val from: KeyguardState,
+    val to: KeyguardState,
+    val animator: ValueAnimator?, // 'null' animator signal manual control
+) {
+    override fun toString(): String =
+        "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " +
+            (if (animator != null) {
+                "animated"
+            } else {
+                "manual"
+            }) +
+            ")"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
new file mode 100644
index 0000000..d8691c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** Possible states for a running transition between [State] */
+enum class TransitionState {
+    NONE,
+    STARTED,
+    RUNNING,
+    FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
new file mode 100644
index 0000000..688ec91
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.keyguard.shared.model
+
+/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */
+data class TransitionStep(
+    val from: KeyguardState = KeyguardState.NONE,
+    val to: KeyguardState = KeyguardState.NONE,
+    val value: Float = 0f, // constrained [0.0, 1.0]
+    val transitionState: TransitionState = TransitionState.NONE,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
new file mode 100644
index 0000000..0645236
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 00bf210..ff291bf 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -43,7 +43,7 @@
     @SysUISingleton
     @DozeLog
     public static LogBuffer provideDozeLogBuffer(LogBufferFactory factory) {
-        return factory.create("DozeLog", 120);
+        return factory.create("DozeLog", 150);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -250,7 +250,7 @@
     /**
      * Provides a buffer for our connections and disconnections to MediaBrowserService.
      *
-     * See {@link com.android.systemui.media.ResumeMediaBrowser}.
+     * See {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser}.
      */
     @Provides
     @SysUISingleton
@@ -262,7 +262,7 @@
     /**
      * Provides a buffer for updates to the media carousel.
      *
-     * See {@link com.android.systemui.media.MediaCarouselController}.
+     * See {@link com.android.systemui.media.controls.ui.MediaCarouselController}.
      */
     @Provides
     @SysUISingleton
@@ -316,6 +316,16 @@
     }
 
     /**
+     * Provides a {@link LogBuffer} for keyguard clock logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardClockLog
+    public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
+        return factory.create("KeyguardClockLog", 500);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
index 90ced02..af43347 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaBrowserLog.java
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.ResumeMediaBrowser}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.resume.ResumeMediaBrowser}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
index e5ac3e2..f4dac6e 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaCarouselController}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
index 99ec05b..0c2cd92 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaTimeoutListenerLog.java
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaTimeoutLogger}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.pipeline.MediaTimeoutLogger}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
index 8c904ea..5b7f4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaViewLog.java
@@ -26,7 +26,7 @@
 import javax.inject.Qualifier;
 
 /**
- * A {@link LogBuffer} for {@link com.android.systemui.media.MediaViewLogger}
+ * A {@link LogBuffer} for {@link com.android.systemui.media.controls.ui.MediaViewLogger}
  */
 @Qualifier
 @Documented
diff --git a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
deleted file mode 100644
index 556560c..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ColorSchemeTransition.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.ArgbEvaluator
-import android.animation.ValueAnimator
-import android.animation.ValueAnimator.AnimatorUpdateListener
-import android.content.Context
-import android.content.res.ColorStateList
-import android.content.res.Configuration
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import android.graphics.drawable.RippleDrawable
-import com.android.internal.R
-import com.android.internal.annotations.VisibleForTesting
-import com.android.settingslib.Utils
-import com.android.systemui.monet.ColorScheme
-
-/**
- * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
- * is triggered.
- */
-interface ColorTransition {
-    fun updateColorScheme(scheme: ColorScheme?): Boolean
-}
-
-/**
- * A [ColorTransition] that animates between two specific colors.
- * It uses a ValueAnimator to execute the animation and interpolate between the source color and
- * the target color.
- *
- * Selection of the target color from the scheme, and application of the interpolated color
- * are delegated to callbacks.
- */
-open class AnimatingColorTransition(
-    private val defaultColor: Int,
-    private val extractColor: (ColorScheme) -> Int,
-    private val applyColor: (Int) -> Unit
-) : AnimatorUpdateListener, ColorTransition {
-
-    private val argbEvaluator = ArgbEvaluator()
-    private val valueAnimator = buildAnimator()
-    var sourceColor: Int = defaultColor
-    var currentColor: Int = defaultColor
-    var targetColor: Int = defaultColor
-
-    override fun onAnimationUpdate(animation: ValueAnimator) {
-        currentColor = argbEvaluator.evaluate(
-            animation.animatedFraction, sourceColor, targetColor
-        ) as Int
-        applyColor(currentColor)
-    }
-
-    override fun updateColorScheme(scheme: ColorScheme?): Boolean {
-        val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme)
-        if (newTargetColor != targetColor) {
-            sourceColor = currentColor
-            targetColor = newTargetColor
-            valueAnimator.cancel()
-            valueAnimator.start()
-            return true
-        }
-        return false
-    }
-
-    init {
-        applyColor(defaultColor)
-    }
-
-    @VisibleForTesting
-    open fun buildAnimator(): ValueAnimator {
-        val animator = ValueAnimator.ofFloat(0f, 1f)
-        animator.duration = 333
-        animator.addUpdateListener(this)
-        return animator
-    }
-}
-
-typealias AnimatingColorTransitionFactory =
-            (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
-
-/**
- * ColorSchemeTransition constructs a ColorTransition for each color in the scheme
- * that needs to be transitioned when changed. It also sets up the assignment functions for sending
- * the sending the interpolated colors to the appropriate views.
- */
-class ColorSchemeTransition internal constructor(
-    private val context: Context,
-    private val mediaViewHolder: MediaViewHolder,
-    animatingColorTransitionFactory: AnimatingColorTransitionFactory
-) {
-    constructor(context: Context, mediaViewHolder: MediaViewHolder) :
-        this(context, mediaViewHolder, ::AnimatingColorTransition)
-
-    val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
-    val surfaceColor = animatingColorTransitionFactory(
-        bgColor,
-        ::surfaceFromScheme
-    ) { surfaceColor ->
-        val colorList = ColorStateList.valueOf(surfaceColor)
-        mediaViewHolder.seamlessIcon.imageTintList = colorList
-        mediaViewHolder.seamlessText.setTextColor(surfaceColor)
-        mediaViewHolder.albumView.backgroundTintList = colorList
-        mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
-    }
-
-    val accentPrimary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        ::accentPrimaryFromScheme
-    ) { accentPrimary ->
-        val accentColorList = ColorStateList.valueOf(accentPrimary)
-        mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
-        mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
-    }
-
-    val accentSecondary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        ::accentSecondaryFromScheme
-    ) { accentSecondary ->
-        val colorList = ColorStateList.valueOf(accentSecondary)
-        (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
-            it.setColor(colorList)
-            it.effectColor = colorList
-        }
-    }
-
-    val colorSeamless = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        { colorScheme: ColorScheme ->
-            // A1-100 dark in dark theme, A1-200 in light theme
-            if (context.resources.configuration.uiMode and
-                    Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES)
-                        colorScheme.accent1[2]
-                        else colorScheme.accent1[3]
-        }, { seamlessColor: Int ->
-            val accentColorList = ColorStateList.valueOf(seamlessColor)
-            mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
-    })
-
-    val textPrimary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimary),
-        ::textPrimaryFromScheme
-    ) { textPrimary ->
-        mediaViewHolder.titleText.setTextColor(textPrimary)
-        val textColorList = ColorStateList.valueOf(textPrimary)
-        mediaViewHolder.seekBar.thumb.setTintList(textColorList)
-        mediaViewHolder.seekBar.progressTintList = textColorList
-        mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
-        mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
-        for (button in mediaViewHolder.getTransparentActionButtons()) {
-            button.imageTintList = textColorList
-        }
-        mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
-    }
-
-    val textPrimaryInverse = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorPrimaryInverse),
-        ::textPrimaryInverseFromScheme
-    ) { textPrimaryInverse ->
-        mediaViewHolder.actionPlayPause.imageTintList = ColorStateList.valueOf(textPrimaryInverse)
-    }
-
-    val textSecondary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorSecondary),
-        ::textSecondaryFromScheme
-    ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) }
-
-    val textTertiary = animatingColorTransitionFactory(
-        loadDefaultColor(R.attr.textColorTertiary),
-        ::textTertiaryFromScheme
-    ) { textTertiary ->
-        mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary)
-    }
-
-    val colorTransitions = arrayOf(
-        surfaceColor,
-        colorSeamless,
-        accentPrimary,
-        accentSecondary,
-        textPrimary,
-        textPrimaryInverse,
-        textSecondary,
-        textTertiary,
-    )
-
-    private fun loadDefaultColor(id: Int): Int {
-        return Utils.getColorAttr(context, id).defaultColor
-    }
-
-    fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
-        var anyChanged = false
-        colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged }
-        colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
-        return anyChanged
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
deleted file mode 100644
index 5977ed0..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ /dev/null
@@ -1,1193 +0,0 @@
-package com.android.systemui.media
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.content.res.ColorStateList
-import android.content.res.Configuration
-import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
-import android.util.Log
-import android.util.MathUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.animation.PathInterpolator
-import android.widget.LinearLayout
-import androidx.annotation.VisibleForTesting
-import com.android.internal.logging.InstanceId
-import com.android.systemui.Dumpable
-import com.android.systemui.R
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.qs.PageIndicator
-import com.android.systemui.shared.system.SysUiStatsLog
-import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.Utils
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.animation.requiresRemeasuring
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.util.traceSection
-import java.io.PrintWriter
-import java.util.TreeMap
-import javax.inject.Inject
-import javax.inject.Provider
-
-private const val TAG = "MediaCarouselController"
-private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
-private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
-
-/**
- * Class that is responsible for keeping the view carousel up to date.
- * This also handles changes in state and applies them to the media carousel like the expansion.
- */
-@SysUISingleton
-class MediaCarouselController @Inject constructor(
-    private val context: Context,
-    private val mediaControlPanelFactory: Provider<MediaControlPanel>,
-    private val visualStabilityProvider: VisualStabilityProvider,
-    private val mediaHostStatesManager: MediaHostStatesManager,
-    private val activityStarter: ActivityStarter,
-    private val systemClock: SystemClock,
-    @Main executor: DelayableExecutor,
-    private val mediaManager: MediaDataManager,
-    configurationController: ConfigurationController,
-    falsingCollector: FalsingCollector,
-    falsingManager: FalsingManager,
-    dumpManager: DumpManager,
-    private val logger: MediaUiEventLogger,
-    private val debugLogger: MediaCarouselControllerLogger
-) : Dumpable {
-    /**
-     * The current width of the carousel
-     */
-    private var currentCarouselWidth: Int = 0
-
-    /**
-     * The current height of the carousel
-     */
-    private var currentCarouselHeight: Int = 0
-
-    /**
-     * Are we currently showing only active players
-     */
-    private var currentlyShowingOnlyActive: Boolean = false
-
-    /**
-     * Is the player currently visible (at the end of the transformation
-     */
-    private var playersVisible: Boolean = false
-    /**
-     * The desired location where we'll be at the end of the transformation. Usually this matches
-     * the end location, except when we're still waiting on a state update call.
-     */
-    @MediaLocation
-    private var desiredLocation: Int = -1
-
-    /**
-     * The ending location of the view where it ends when all animations and transitions have
-     * finished
-     */
-    @MediaLocation
-    @VisibleForTesting
-    var currentEndLocation: Int = -1
-
-    /**
-     * The ending location of the view where it ends when all animations and transitions have
-     * finished
-     */
-    @MediaLocation
-    private var currentStartLocation: Int = -1
-
-    /**
-     * The progress of the transition or 1.0 if there is no transition happening
-     */
-    private var currentTransitionProgress: Float = 1.0f
-
-    /**
-     * The measured width of the carousel
-     */
-    private var carouselMeasureWidth: Int = 0
-
-    /**
-     * The measured height of the carousel
-     */
-    private var carouselMeasureHeight: Int = 0
-    private var desiredHostState: MediaHostState? = null
-    private val mediaCarousel: MediaScrollView
-    val mediaCarouselScrollHandler: MediaCarouselScrollHandler
-    val mediaFrame: ViewGroup
-    @VisibleForTesting
-    lateinit var settingsButton: View
-        private set
-    private val mediaContent: ViewGroup
-    @VisibleForTesting
-    val pageIndicator: PageIndicator
-    private val visualStabilityCallback: OnReorderingAllowedListener
-    private var needsReordering: Boolean = false
-    private var keysNeedRemoval = mutableSetOf<String>()
-    var shouldScrollToKey: Boolean = false
-    private var isRtl: Boolean = false
-        set(value) {
-            if (value != field) {
-                field = value
-                mediaFrame.layoutDirection =
-                        if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
-                mediaCarouselScrollHandler.scrollToStart()
-            }
-        }
-    private var currentlyExpanded = true
-        set(value) {
-            if (field != value) {
-                field = value
-                for (player in MediaPlayerData.players()) {
-                    player.setListening(field)
-                }
-            }
-        }
-
-    companion object {
-        const val ANIMATION_BASE_DURATION = 2200f
-        const val DURATION = 167f
-        const val DETAILS_DELAY = 1067f
-        const val CONTROLS_DELAY = 1400f
-        const val PAGINATION_DELAY = 1900f
-        const val MEDIATITLES_DELAY = 1000f
-        const val MEDIACONTAINERS_DELAY = 967f
-        val TRANSFORM_BEZIER = PathInterpolator (0.68F, 0F, 0F, 1F)
-        val REVERSE_BEZIER = PathInterpolator (0F, 0.68F, 1F, 0F)
-
-        fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
-            val transformStartFraction = delay / ANIMATION_BASE_DURATION
-            val transformDurationFraction = duration / ANIMATION_BASE_DURATION
-            val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
-            return MathUtils.constrain((squishinessToTime - transformStartFraction) /
-                    transformDurationFraction, 0F, 1F)
-        }
-    }
-
-    private val configListener = object : ConfigurationController.ConfigurationListener {
-        override fun onDensityOrFontScaleChanged() {
-            // System font changes should only happen when UMO is offscreen or a flicker may occur
-            updatePlayers(recreateMedia = true)
-            inflateSettingsButton()
-        }
-
-        override fun onThemeChanged() {
-            updatePlayers(recreateMedia = false)
-            inflateSettingsButton()
-        }
-
-        override fun onConfigChanged(newConfig: Configuration?) {
-            if (newConfig == null) return
-            isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
-        }
-
-        override fun onUiModeChanged() {
-            updatePlayers(recreateMedia = false)
-            inflateSettingsButton()
-        }
-    }
-
-    /**
-     * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
-     * It will be called when the container is out of view.
-     */
-    lateinit var updateUserVisibility: () -> Unit
-    lateinit var updateHostVisibility: () -> Unit
-
-    private val isReorderingAllowed: Boolean
-        get() = visualStabilityProvider.isReorderingAllowed
-
-    init {
-        dumpManager.registerDumpable(TAG, this)
-        mediaFrame = inflateMediaCarousel()
-        mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
-        pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
-        mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
-                executor, this::onSwipeToDismiss, this::updatePageIndicatorLocation,
-                this::closeGuts, falsingCollector, falsingManager, this::logSmartspaceImpression,
-                logger)
-        isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
-        inflateSettingsButton()
-        mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
-        configurationController.addCallback(configListener)
-        visualStabilityCallback = OnReorderingAllowedListener {
-            if (needsReordering) {
-                needsReordering = false
-                reorderAllPlayers(previousVisiblePlayerKey = null)
-            }
-
-            keysNeedRemoval.forEach {
-                removePlayer(it)
-            }
-            if (keysNeedRemoval.size > 0) {
-                // Carousel visibility may need to be updated after late removals
-                updateHostVisibility()
-            }
-            keysNeedRemoval.clear()
-
-            // Update user visibility so that no extra impression will be logged when
-            // activeMediaIndex resets to 0
-            if (this::updateUserVisibility.isInitialized) {
-                updateUserVisibility()
-            }
-
-            // Let's reset our scroll position
-            mediaCarouselScrollHandler.scrollToStart()
-        }
-        visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
-        mediaManager.addListener(object : MediaDataManager.Listener {
-            override fun onMediaDataLoaded(
-                key: String,
-                oldKey: String?,
-                data: MediaData,
-                immediately: Boolean,
-                receivedSmartspaceCardLatency: Int,
-                isSsReactivated: Boolean
-            ) {
-                debugLogger.logMediaLoaded(key)
-                if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
-                    // Log card received if a new resumable media card is added
-                    MediaPlayerData.getMediaPlayer(key)?.let {
-                        /* ktlint-disable max-line-length */
-                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                it.mSmartspaceId,
-                                it.mUid,
-                                surfaces = intArrayOf(
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                rank = MediaPlayerData.getMediaPlayerIndex(key))
-                        /* ktlint-disable max-line-length */
-                    }
-                    if (mediaCarouselScrollHandler.visibleToUser &&
-                            mediaCarouselScrollHandler.visibleMediaIndex
-                            == MediaPlayerData.getMediaPlayerIndex(key)) {
-                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
-                    }
-                } else if (receivedSmartspaceCardLatency != 0) {
-                    // Log resume card received if resumable media card is reactivated and
-                    // resume card is ranked first
-                    MediaPlayerData.players().forEachIndexed { index, it ->
-                        if (it.recommendationViewHolder == null) {
-                            it.mSmartspaceId = SmallHash.hash(it.mUid +
-                                    systemClock.currentTimeMillis().toInt())
-                            it.mIsImpressed = false
-                            /* ktlint-disable max-line-length */
-                            logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                    it.mSmartspaceId,
-                                    it.mUid,
-                                    surfaces = intArrayOf(
-                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                            SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                    rank = index,
-                                    receivedLatencyMillis = receivedSmartspaceCardLatency)
-                            /* ktlint-disable max-line-length */
-                        }
-                    }
-                    // If media container area already visible to the user, log impression for
-                    // reactivated card.
-                    if (mediaCarouselScrollHandler.visibleToUser &&
-                            !mediaCarouselScrollHandler.qsExpanded) {
-                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
-                    }
-                }
-
-                val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
-                if (canRemove && !Utils.useMediaResumption(context)) {
-                    // This view isn't playing, let's remove this! This happens e.g when
-                    // dismissing/timing out a view. We still have the data around because
-                    // resumption could be on, but we should save the resources and release this.
-                    if (isReorderingAllowed) {
-                        onMediaDataRemoved(key)
-                    } else {
-                        keysNeedRemoval.add(key)
-                    }
-                } else {
-                    keysNeedRemoval.remove(key)
-                }
-            }
-
-            override fun onSmartspaceMediaDataLoaded(
-                key: String,
-                data: SmartspaceMediaData,
-                shouldPrioritize: Boolean
-            ) {
-                debugLogger.logRecommendationLoaded(key)
-                // Log the case where the hidden media carousel with the existed inactive resume
-                // media is shown by the Smartspace signal.
-                if (data.isActive) {
-                    val hasActivatedExistedResumeMedia =
-                            !mediaManager.hasActiveMedia() &&
-                                    mediaManager.hasAnyMedia() &&
-                                    shouldPrioritize
-                    if (hasActivatedExistedResumeMedia) {
-                        // Log resume card received if resumable media card is reactivated and
-                        // recommendation card is valid and ranked first
-                        MediaPlayerData.players().forEachIndexed { index, it ->
-                            if (it.recommendationViewHolder == null) {
-                                it.mSmartspaceId = SmallHash.hash(it.mUid +
-                                        systemClock.currentTimeMillis().toInt())
-                                it.mIsImpressed = false
-                                /* ktlint-disable max-line-length */
-                                logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                        it.mSmartspaceId,
-                                        it.mUid,
-                                        surfaces = intArrayOf(
-                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                        rank = index,
-                                        receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
-                                /* ktlint-disable max-line-length */
-                            }
-                        }
-                    }
-                    addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
-                    MediaPlayerData.getMediaPlayer(key)?.let {
-                        /* ktlint-disable max-line-length */
-                        logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED
-                                it.mSmartspaceId,
-                                it.mUid,
-                                surfaces = intArrayOf(
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
-                                        SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY),
-                                rank = MediaPlayerData.getMediaPlayerIndex(key),
-                                receivedLatencyMillis = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis).toInt())
-                        /* ktlint-disable max-line-length */
-                    }
-                    if (mediaCarouselScrollHandler.visibleToUser &&
-                            mediaCarouselScrollHandler.visibleMediaIndex
-                            == MediaPlayerData.getMediaPlayerIndex(key)) {
-                        logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
-                    }
-                } else {
-                    onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
-                }
-            }
-
-            override fun onMediaDataRemoved(key: String) {
-                debugLogger.logMediaRemoved(key)
-                removePlayer(key)
-            }
-
-            override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-                debugLogger.logRecommendationRemoved(key, immediately)
-                if (immediately || isReorderingAllowed) {
-                    removePlayer(key)
-                    if (!immediately) {
-                        // Although it wasn't requested, we were able to process the removal
-                        // immediately since reordering is allowed. So, notify hosts to update
-                        if (this@MediaCarouselController::updateHostVisibility.isInitialized) {
-                            updateHostVisibility()
-                        }
-                    }
-                } else {
-                    keysNeedRemoval.add(key)
-                }
-            }
-        })
-        mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
-            // The pageIndicator is not laid out yet when we get the current state update,
-            // Lets make sure we have the right dimensions
-            updatePageIndicatorLocation()
-        }
-        mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
-            override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
-                if (location == desiredLocation) {
-                    onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
-                }
-            }
-        })
-    }
-
-    private fun inflateSettingsButton() {
-        val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
-                mediaFrame, false) as View
-        if (this::settingsButton.isInitialized) {
-            mediaFrame.removeView(settingsButton)
-        }
-        settingsButton = settings
-        mediaFrame.addView(settingsButton)
-        mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
-        settingsButton.setOnClickListener {
-            logger.logCarouselSettings()
-            activityStarter.startActivity(settingsIntent, true /* dismissShade */)
-        }
-    }
-
-    private fun inflateMediaCarousel(): ViewGroup {
-        val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel,
-                UniqueObjectHostView(context), false) as ViewGroup
-        // Because this is inflated when not attached to the true view hierarchy, it resolves some
-        // potential issues to force that the layout direction is defined by the locale
-        // (rather than inherited from the parent, which would resolve to LTR when unattached).
-        mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-        return mediaCarousel
-    }
-
-    private fun reorderAllPlayers(
-            previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
-            key: String? = null
-    ) {
-        mediaContent.removeAllViews()
-        for (mediaPlayer in MediaPlayerData.players()) {
-            mediaPlayer.mediaViewHolder?.let {
-                mediaContent.addView(it.player)
-            } ?: mediaPlayer.recommendationViewHolder?.let {
-                mediaContent.addView(it.recommendations)
-            }
-        }
-        mediaCarouselScrollHandler.onPlayersChanged()
-        MediaPlayerData.updateVisibleMediaPlayers()
-        // Automatically scroll to the active player if needed
-        if (shouldScrollToKey) {
-            shouldScrollToKey = false
-            val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
-            if (mediaIndex != -1) {
-                previousVisiblePlayerKey?.let {
-                    val previousVisibleIndex = MediaPlayerData.playerKeys()
-                            .indexOfFirst { key -> it == key }
-                    mediaCarouselScrollHandler
-                            .scrollToPlayer(previousVisibleIndex, mediaIndex)
-                } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
-            }
-        }
-    }
-
-    // Returns true if new player is added
-    private fun addOrUpdatePlayer(
-        key: String,
-        oldKey: String?,
-        data: MediaData,
-        isSsReactivated: Boolean
-    ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") {
-        MediaPlayerData.moveIfExists(oldKey, key)
-        val existingPlayer = MediaPlayerData.getMediaPlayer(key)
-        val curVisibleMediaKey = MediaPlayerData.visiblePlayerKeys()
-                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-        if (existingPlayer == null) {
-            val newPlayer = mediaControlPanelFactory.get()
-            newPlayer.attachPlayer(MediaViewHolder.create(
-                    LayoutInflater.from(context), mediaContent))
-            newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-            val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                    ViewGroup.LayoutParams.WRAP_CONTENT)
-            newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
-            newPlayer.bindPlayer(data, key)
-            newPlayer.setListening(currentlyExpanded)
-            MediaPlayerData.addMediaPlayer(
-                key, data, newPlayer, systemClock, isSsReactivated, debugLogger
-            )
-            updatePlayerToState(newPlayer, noAnimation = true)
-            // Media data added from a recommendation card should starts playing.
-            if ((shouldScrollToKey && data.isPlaying == true) ||
-                    (!shouldScrollToKey && data.active)) {
-                reorderAllPlayers(curVisibleMediaKey, key)
-            } else {
-                needsReordering = true
-            }
-        } else {
-            existingPlayer.bindPlayer(data, key)
-            MediaPlayerData.addMediaPlayer(
-                key, data, existingPlayer, systemClock, isSsReactivated, debugLogger
-            )
-            val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
-            // In case of recommendations hits.
-            // Check the playing status of media player and the package name.
-            // To make sure we scroll to the right app's media player.
-            if (isReorderingAllowed ||
-                    shouldScrollToKey &&
-                    data.isPlaying == true &&
-                    packageName == data.packageName
-            ) {
-                reorderAllPlayers(curVisibleMediaKey, key)
-            } else {
-                needsReordering = true
-            }
-        }
-        updatePageIndicator()
-        mediaCarouselScrollHandler.onPlayersChanged()
-        mediaFrame.requiresRemeasuring = true
-        // Check postcondition: mediaContent should have the same number of children as there are
-        // elements in mediaPlayers.
-        if (MediaPlayerData.players().size != mediaContent.childCount) {
-            Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
-        }
-        return existingPlayer == null
-    }
-
-    private fun addSmartspaceMediaRecommendations(
-        key: String,
-        data: SmartspaceMediaData,
-        shouldPrioritize: Boolean
-    ) = traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
-        if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
-        if (MediaPlayerData.getMediaPlayer(key) != null) {
-            Log.w(TAG, "Skip adding smartspace target in carousel")
-            return
-        }
-
-        val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
-        existingSmartspaceMediaKey?.let {
-            val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true)
-            removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) }
-        }
-
-        val newRecs = mediaControlPanelFactory.get()
-        newRecs.attachRecommendation(
-                RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent))
-        newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
-        val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                ViewGroup.LayoutParams.WRAP_CONTENT)
-        newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
-        newRecs.bindRecommendation(data)
-        val curVisibleMediaKey = MediaPlayerData.visiblePlayerKeys()
-                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-        MediaPlayerData.addMediaRecommendation(
-            key, data, newRecs, shouldPrioritize, systemClock, debugLogger
-        )
-        updatePlayerToState(newRecs, noAnimation = true)
-        reorderAllPlayers(curVisibleMediaKey)
-        updatePageIndicator()
-        mediaFrame.requiresRemeasuring = true
-        // Check postcondition: mediaContent should have the same number of children as there are
-        // elements in mediaPlayers.
-        if (MediaPlayerData.players().size != mediaContent.childCount) {
-            Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
-        }
-    }
-
-    fun removePlayer(
-        key: String,
-        dismissMediaData: Boolean = true,
-        dismissRecommendation: Boolean = true
-    ) {
-        if (key == MediaPlayerData.smartspaceMediaKey()) {
-            MediaPlayerData.smartspaceMediaData?.let {
-                logger.logRecommendationRemoved(it.packageName, it.instanceId)
-            }
-        }
-        val removed = MediaPlayerData.removeMediaPlayer(
-                key,
-                dismissMediaData || dismissRecommendation
-        )
-        removed?.apply {
-            mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
-            mediaContent.removeView(removed.mediaViewHolder?.player)
-            mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
-            removed.onDestroy()
-            mediaCarouselScrollHandler.onPlayersChanged()
-            updatePageIndicator()
-
-            if (dismissMediaData) {
-                // Inform the media manager of a potentially late dismissal
-                mediaManager.dismissMediaData(key, delay = 0L)
-            }
-            if (dismissRecommendation) {
-                // Inform the media manager of a potentially late dismissal
-                mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
-            }
-        }
-    }
-
-    private fun updatePlayers(recreateMedia: Boolean) {
-        pageIndicator.tintList = ColorStateList.valueOf(
-            context.getColor(R.color.media_paging_indicator)
-        )
-
-        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
-            if (isSsMediaRec) {
-                val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
-                removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
-                smartspaceMediaData?.let {
-                    addSmartspaceMediaRecommendations(
-                            it.targetId, it, MediaPlayerData.shouldPrioritizeSs)
-                }
-            } else {
-                val isSsReactivated = MediaPlayerData.isSsReactivated(key)
-                if (recreateMedia) {
-                    removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
-                }
-                addOrUpdatePlayer(
-                        key = key, oldKey = null, data = data, isSsReactivated = isSsReactivated)
-            }
-        }
-    }
-
-    private fun updatePageIndicator() {
-        val numPages = mediaContent.getChildCount()
-        pageIndicator.setNumPages(numPages)
-        if (numPages == 1) {
-            pageIndicator.setLocation(0f)
-        }
-        updatePageIndicatorAlpha()
-    }
-
-    /**
-     * Set a new interpolated state for all players. This is a state that is usually controlled
-     * by a finger movement where the user drags from one state to the next.
-     *
-     * @param startLocation the start location of our state or -1 if this is directly set
-     * @param endLocation the ending location of our state.
-     * @param progress the progress of the transition between startLocation and endlocation. If
-     *                 this is not a guided transformation, this will be 1.0f
-     * @param immediately should this state be applied immediately, canceling all animations?
-     */
-    fun setCurrentState(
-        @MediaLocation startLocation: Int,
-        @MediaLocation endLocation: Int,
-        progress: Float,
-        immediately: Boolean
-    ) {
-        if (startLocation != currentStartLocation ||
-                endLocation != currentEndLocation ||
-                progress != currentTransitionProgress ||
-                immediately
-        ) {
-            currentStartLocation = startLocation
-            currentEndLocation = endLocation
-            currentTransitionProgress = progress
-            for (mediaPlayer in MediaPlayerData.players()) {
-                updatePlayerToState(mediaPlayer, immediately)
-            }
-            maybeResetSettingsCog()
-            updatePageIndicatorAlpha()
-        }
-    }
-
-    @VisibleForTesting
-    fun updatePageIndicatorAlpha() {
-        val hostStates = mediaHostStatesManager.mediaHostStates
-        val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
-        val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
-        val startAlpha = if (startIsVisible) 1.0f else 0.0f
-        // when squishing in split shade, only use endState, which keeps changing
-        // to provide squishFraction
-        val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
-        val endAlpha = (if (endIsVisible) 1.0f else 0.0f) *
-                calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
-        var alpha = 1.0f
-        if (!endIsVisible || !startIsVisible) {
-            var progress = currentTransitionProgress
-            if (!endIsVisible) {
-                progress = 1.0f - progress
-            }
-            // Let's fade in quickly at the end where the view is visible
-            progress = MathUtils.constrain(
-                    MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress),
-                    0.0f,
-                    1.0f)
-            alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
-        }
-        pageIndicator.alpha = alpha
-    }
-
-    private fun updatePageIndicatorLocation() {
-        // Update the location of the page indicator, carousel clipping
-        val translationX = if (isRtl) {
-            (pageIndicator.width - currentCarouselWidth) / 2.0f
-        } else {
-            (currentCarouselWidth - pageIndicator.width) / 2.0f
-        }
-        pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
-        val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
-        pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
-                layoutParams.bottomMargin).toFloat()
-    }
-
-    /**
-     * Update the dimension of this carousel.
-     */
-    private fun updateCarouselDimensions() {
-        var width = 0
-        var height = 0
-        for (mediaPlayer in MediaPlayerData.players()) {
-            val controller = mediaPlayer.mediaViewController
-            // When transitioning the view to gone, the view gets smaller, but the translation
-            // Doesn't, let's add the translation
-            width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
-            height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
-        }
-        if (width != currentCarouselWidth || height != currentCarouselHeight) {
-            currentCarouselWidth = width
-            currentCarouselHeight = height
-            mediaCarouselScrollHandler.setCarouselBounds(
-                    currentCarouselWidth, currentCarouselHeight)
-            updatePageIndicatorLocation()
-            updatePageIndicatorAlpha()
-        }
-    }
-
-    private fun maybeResetSettingsCog() {
-        val hostStates = mediaHostStatesManager.mediaHostStates
-        val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
-                ?: true
-        val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
-                ?: endShowsActive
-        if (currentlyShowingOnlyActive != endShowsActive ||
-                ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
-                        startShowsActive != endShowsActive)) {
-            // Whenever we're transitioning from between differing states or the endstate differs
-            // we reset the translation
-            currentlyShowingOnlyActive = endShowsActive
-            mediaCarouselScrollHandler.resetTranslation(animate = true)
-        }
-    }
-
-    private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
-        mediaPlayer.mediaViewController.setCurrentState(
-                startLocation = currentStartLocation,
-                endLocation = currentEndLocation,
-                transitionProgress = currentTransitionProgress,
-                applyImmediately = noAnimation)
-    }
-
-    /**
-     * The desired location of this view has changed. We should remeasure the view to match
-     * the new bounds and kick off bounds animations if necessary.
-     * If an animation is happening, an animation is kicked of externally, which sets a new
-     * current state until we reach the targetState.
-     *
-     * @param desiredLocation the location we're going to
-     * @param desiredHostState the target state we're transitioning to
-     * @param animate should this be animated
-     */
-    fun onDesiredLocationChanged(
-        desiredLocation: Int,
-        desiredHostState: MediaHostState?,
-        animate: Boolean,
-        duration: Long = 200,
-        startDelay: Long = 0
-    ) = traceSection("MediaCarouselController#onDesiredLocationChanged") {
-        desiredHostState?.let {
-            if (this.desiredLocation != desiredLocation) {
-                // Only log an event when location changes
-                logger.logCarouselPosition(desiredLocation)
-            }
-
-            // This is a hosting view, let's remeasure our players
-            this.desiredLocation = desiredLocation
-            this.desiredHostState = it
-            currentlyExpanded = it.expansion > 0
-
-            val shouldCloseGuts = !currentlyExpanded &&
-                    !mediaManager.hasActiveMediaOrRecommendation() &&
-                    desiredHostState.showsOnlyActiveMedia
-
-            for (mediaPlayer in MediaPlayerData.players()) {
-                if (animate) {
-                    mediaPlayer.mediaViewController.animatePendingStateChange(
-                            duration = duration,
-                            delay = startDelay)
-                }
-                if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
-                    mediaPlayer.closeGuts(!animate)
-                }
-
-                mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
-            }
-            mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
-            mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
-            val nowVisible = it.visible
-            if (nowVisible != playersVisible) {
-                playersVisible = nowVisible
-                if (nowVisible) {
-                    mediaCarouselScrollHandler.resetTranslation()
-                }
-            }
-            updateCarouselSize()
-        }
-    }
-
-    fun closeGuts(immediate: Boolean = true) {
-        MediaPlayerData.players().forEach {
-            it.closeGuts(immediate)
-        }
-    }
-
-    /**
-     * Update the size of the carousel, remeasuring it if necessary.
-     */
-    private fun updateCarouselSize() {
-        val width = desiredHostState?.measurementInput?.width ?: 0
-        val height = desiredHostState?.measurementInput?.height ?: 0
-        if (width != carouselMeasureWidth && width != 0 ||
-                height != carouselMeasureHeight && height != 0) {
-            carouselMeasureWidth = width
-            carouselMeasureHeight = height
-            val playerWidthPlusPadding = carouselMeasureWidth +
-                    context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
-            // Let's remeasure the carousel
-            val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
-            val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
-            mediaCarousel.measure(widthSpec, heightSpec)
-            mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
-            // Update the padding after layout; view widths are used in RTL to calculate scrollX
-            mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
-        }
-    }
-
-    /**
-     * Log the user impression for media card at visibleMediaIndex.
-     */
-    fun logSmartspaceImpression(qsExpanded: Boolean) {
-        val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
-        if (MediaPlayerData.players().size > visibleMediaIndex) {
-            val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex)
-            val hasActiveMediaOrRecommendationCard =
-                    MediaPlayerData.hasActiveMediaOrRecommendationCard()
-            if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
-                // Skip logging if on LS or QQS, and there is no active media card
-                return
-            }
-            mediaControlPanel?.let {
-                logSmartspaceCardReported(800, // SMARTSPACE_CARD_SEEN
-                        it.mSmartspaceId,
-                        it.mUid,
-                        intArrayOf(it.surfaceForSmartspaceLogging))
-                it.mIsImpressed = true
-            }
-        }
-    }
-
-    @JvmOverloads
-    /**
-     * Log Smartspace events
-     *
-     * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
-     * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
-     * instanceId
-     * @param uid uid for the application that media comes from
-     * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
-     * the event happened
-     * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
-     * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
-     * @param interactedSubcardCardinality how many media items were shown to the user when there
-     * is user interaction
-     * @param rank the rank for media card in the media carousel, starting from 0
-     * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
-     * between headphone connection to sysUI displays media recommendation card
-     * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
-     *
-     */
-    fun logSmartspaceCardReported(
-        eventId: Int,
-        instanceId: Int,
-        uid: Int,
-        surfaces: IntArray,
-        interactedSubcardRank: Int = 0,
-        interactedSubcardCardinality: Int = 0,
-        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
-        receivedLatencyMillis: Int = 0,
-        isSwipeToDismiss: Boolean = false
-    ) {
-        if (MediaPlayerData.players().size <= rank) {
-            return
-        }
-
-        val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank)
-        // Only log media resume card when Smartspace data is available
-        if (!mediaControlKey.isSsMediaRec &&
-                !mediaManager.smartspaceMediaData.isActive &&
-                MediaPlayerData.smartspaceMediaData == null) {
-            return
-        }
-
-        val cardinality = mediaContent.getChildCount()
-        surfaces.forEach { surface ->
-            /* ktlint-disable max-line-length */
-            SysUiStatsLog.write(SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
-                    eventId,
-                    instanceId,
-                    // Deprecated, replaced with AiAi feature type so we don't need to create logging
-                    // card type for each new feature.
-                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
-                    surface,
-                    // Use -1 as rank value to indicate user swipe to dismiss the card
-                    if (isSwipeToDismiss) -1 else rank,
-                    cardinality,
-                    if (mediaControlKey.isSsMediaRec)
-                        15 // MEDIA_RECOMMENDATION
-                    else if (mediaControlKey.isSsReactivated)
-                        43 // MEDIA_RESUME_SS_ACTIVATED
-                    else
-                        31, // MEDIA_RESUME
-                    uid,
-                    interactedSubcardRank,
-                    interactedSubcardCardinality,
-                    receivedLatencyMillis,
-                    null, // Media cards cannot have subcards.
-                    null // Media cards don't have dimensions today.
-            )
-            /* ktlint-disable max-line-length */
-            if (DEBUG) {
-                Log.d(TAG, "Log Smartspace card event id: $eventId instance id: $instanceId" +
-                        " surface: $surface rank: $rank cardinality: $cardinality " +
-                        "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
-                        "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
-                        "uid: $uid " +
-                        "interactedSubcardRank: $interactedSubcardRank " +
-                        "interactedSubcardCardinality: $interactedSubcardCardinality " +
-                        "received_latency_millis: $receivedLatencyMillis")
-            }
-        }
-    }
-
-    private fun onSwipeToDismiss() {
-        MediaPlayerData.players().forEachIndexed {
-            index, it ->
-            if (it.mIsImpressed) {
-                logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT,
-                        it.mSmartspaceId,
-                        it.mUid,
-                        intArrayOf(it.surfaceForSmartspaceLogging),
-                        rank = index,
-                        isSwipeToDismiss = true)
-                // Reset card impressed state when swipe to dismissed
-                it.mIsImpressed = false
-            }
-        }
-        logger.logSwipeDismiss()
-        mediaManager.onSwipeToDismiss()
-    }
-
-    fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
-        return MediaPlayerData.playerKeys()
-                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent
-    }
-
-    override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("keysNeedRemoval: $keysNeedRemoval")
-            println("dataKeys: ${MediaPlayerData.dataKeys()}")
-            println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
-            println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
-            println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
-            println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
-            println("current size: $currentCarouselWidth x $currentCarouselHeight")
-            println("location: $desiredLocation")
-            println("state: ${desiredHostState?.expansion}, " +
-                "only active ${desiredHostState?.showsOnlyActiveMedia}")
-        }
-    }
-}
-
-@VisibleForTesting
-internal object MediaPlayerData {
-    private val EMPTY = MediaData(
-            userId = -1,
-            initialized = false,
-            app = null,
-            appIcon = null,
-            artist = null,
-            song = null,
-            artwork = null,
-            actions = emptyList(),
-            actionsToShowInCompact = emptyList(),
-            packageName = "INVALID",
-            token = null,
-            clickIntent = null,
-            device = null,
-            active = true,
-            resumeAction = null,
-            instanceId = InstanceId.fakeInstanceId(-1),
-            appUid = -1)
-    // Whether should prioritize Smartspace card.
-    internal var shouldPrioritizeSs: Boolean = false
-        private set
-    internal var smartspaceMediaData: SmartspaceMediaData? = null
-        private set
-
-    data class MediaSortKey(
-        val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
-        val data: MediaData,
-        val key: String,
-        val updateTime: Long = 0,
-        val isSsReactivated: Boolean = false
-    )
-
-    private val comparator = compareByDescending<MediaSortKey> {
-            it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL }
-        .thenByDescending {
-            it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL }
-        .thenByDescending { it.data.active }
-        .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
-        .thenByDescending { !it.data.resumption }
-        .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
-        .thenByDescending { it.data.lastActive }
-        .thenByDescending { it.updateTime }
-        .thenByDescending { it.data.notificationKey }
-
-    private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
-    private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
-    // A map that tracks order of visible media players before they get reordered.
-    private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
-
-    fun addMediaPlayer(
-        key: String,
-        data: MediaData,
-        player: MediaControlPanel,
-        clock: SystemClock,
-        isSsReactivated: Boolean,
-        debugLogger: MediaCarouselControllerLogger? = null
-    ) {
-        val removedPlayer = removeMediaPlayer(key)
-        if (removedPlayer != null && removedPlayer != player) {
-            debugLogger?.logPotentialMemoryLeak(key)
-        }
-        val sortKey = MediaSortKey(isSsMediaRec = false,
-                data, key, clock.currentTimeMillis(), isSsReactivated = isSsReactivated)
-        mediaData.put(key, sortKey)
-        mediaPlayers.put(sortKey, player)
-        visibleMediaPlayers.put(key, sortKey)
-    }
-
-    fun addMediaRecommendation(
-        key: String,
-        data: SmartspaceMediaData,
-        player: MediaControlPanel,
-        shouldPrioritize: Boolean,
-        clock: SystemClock,
-        debugLogger: MediaCarouselControllerLogger? = null
-    ) {
-        shouldPrioritizeSs = shouldPrioritize
-        val removedPlayer = removeMediaPlayer(key)
-        if (removedPlayer != null && removedPlayer != player) {
-            debugLogger?.logPotentialMemoryLeak(key)
-        }
-        val sortKey = MediaSortKey(
-            isSsMediaRec = true,
-            EMPTY.copy(isPlaying = false),
-            key,
-            clock.currentTimeMillis(),
-            isSsReactivated = true
-        )
-        mediaData.put(key, sortKey)
-        mediaPlayers.put(sortKey, player)
-        visibleMediaPlayers.put(key, sortKey)
-        smartspaceMediaData = data
-    }
-
-    fun moveIfExists(
-        oldKey: String?,
-        newKey: String,
-        debugLogger: MediaCarouselControllerLogger? = null
-    ) {
-        if (oldKey == null || oldKey == newKey) {
-            return
-        }
-
-        mediaData.remove(oldKey)?.let {
-            // MediaPlayer should not be visible
-            // no need to set isDismissed flag.
-            val removedPlayer = removeMediaPlayer(newKey)
-            removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
-            mediaData.put(newKey, it)
-        }
-    }
-
-    fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? {
-        return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex))
-    }
-
-    fun getMediaPlayer(key: String): MediaControlPanel? {
-        return mediaData.get(key)?.let { mediaPlayers.get(it) }
-    }
-
-    fun getMediaPlayerIndex(key: String): Int {
-        val sortKey = mediaData.get(key)
-        mediaPlayers.entries.forEachIndexed { index, e ->
-            if (e.key == sortKey) {
-                return index
-            }
-        }
-        return -1
-    }
-
-    /**
-     * Removes media player given the key.
-     * @param isDismissed determines whether the media player is removed from the carousel.
-     */
-    fun removeMediaPlayer(key: String, isDismissed: Boolean = false) = mediaData.remove(key)?.let {
-        if (it.isSsMediaRec) {
-            smartspaceMediaData = null
-        }
-        if (isDismissed) {
-            visibleMediaPlayers.remove(key)
-        }
-        mediaPlayers.remove(it)
-    }
-
-    fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
-
-    fun dataKeys() = mediaData.keys
-
-    fun players() = mediaPlayers.values
-
-    fun playerKeys() = mediaPlayers.keys
-
-    fun visiblePlayerKeys() = visibleMediaPlayers.values
-
-    /** Returns the index of the first non-timeout media. */
-    fun firstActiveMediaIndex(): Int {
-        mediaPlayers.entries.forEachIndexed { index, e ->
-            if (!e.key.isSsMediaRec && e.key.data.active) {
-                return index
-            }
-        }
-        return -1
-    }
-
-    /** Returns the existing Smartspace target id. */
-    fun smartspaceMediaKey(): String? {
-        mediaData.entries.forEach { e ->
-            if (e.value.isSsMediaRec) {
-                return e.key
-            }
-        }
-        return null
-    }
-
-    @VisibleForTesting
-    fun clear() {
-        mediaData.clear()
-        mediaPlayers.clear()
-        visibleMediaPlayers.clear()
-    }
-
-    /* Returns true if there is active media player card or recommendation card */
-    fun hasActiveMediaOrRecommendationCard(): Boolean {
-        if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
-            return true
-        }
-        if (firstActiveMediaIndex() != -1) {
-            return true
-        }
-        return false
-    }
-
-    fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
-
-    /**
-     * This method is called when media players are reordered.
-     * To make sure we have the new version of the order of
-     * media players visible to user.
-     */
-    fun updateVisibleMediaPlayers() {
-        visibleMediaPlayers.clear()
-        playerKeys().forEach {
-            visibleMediaPlayers.put(it.key, it)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
deleted file mode 100644
index d40624b..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.dagger.MediaCarouselControllerLog
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import javax.inject.Inject
-
-/** A debug logger for [MediaCarouselController]. */
-@SysUISingleton
-class MediaCarouselControllerLogger @Inject constructor(
-    @MediaCarouselControllerLog private val buffer: LogBuffer
-) {
-    /**
-     * Log that there might be a potential memory leak for the [MediaControlPanel] and/or
-     * [MediaViewController] related to [key].
-     */
-    fun logPotentialMemoryLeak(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        {
-            "Potential memory leak: " +
-                    "Removing control panel for $str1 from map without calling #onDestroy"
-        }
-    )
-
-    fun logMediaLoaded(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        { "add player $str1" }
-    )
-
-    fun logMediaRemoved(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        { "removing player $str1" }
-    )
-
-    fun logRecommendationLoaded(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        { str1 = key },
-        { "add recommendation $str1" }
-    )
-
-    fun logRecommendationRemoved(key: String, immediately: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-            bool1 = immediately
-        },
-        { "removing recommendation $str1, immediate=$bool1" }
-    )
-}
-
-private const val TAG = "MediaCarouselCtlrLog"
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
deleted file mode 100644
index e0b6d1f..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ /dev/null
@@ -1,1243 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.annotation.IntDef
-import android.content.Context
-import android.content.res.Configuration
-import android.database.ContentObserver
-import android.graphics.Rect
-import android.net.Uri
-import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
-import android.util.Log
-import android.util.MathUtils
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroupOverlay
-import androidx.annotation.VisibleForTesting
-import com.android.keyguard.KeyguardViewController
-import com.android.systemui.R
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dreams.DreamOverlayStateController
-import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.media.dream.MediaDreamComplication
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.shade.NotifPanelEvents
-import com.android.systemui.statusbar.CrossFadeHelper
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator
-import com.android.systemui.statusbar.phone.KeyguardBypassController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.LargeScreenUtils
-import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-private val TAG: String = MediaHierarchyManager::class.java.simpleName
-
-/**
- * Similarly to isShown but also excludes views that have 0 alpha
- */
-val View.isShownNotFaded: Boolean
-    get() {
-        var current: View = this
-        while (true) {
-            if (current.visibility != View.VISIBLE) {
-                return false
-            }
-            if (current.alpha == 0.0f) {
-                return false
-            }
-            val parent = current.parent ?: return false // We are not attached to the view root
-            if (parent !is View) {
-                // we reached the viewroot, hurray
-                return true
-            }
-            current = parent
-        }
-    }
-
-/**
- * This manager is responsible for placement of the unique media view between the different hosts
- * and animate the positions of the views to achieve seamless transitions.
- */
-@SysUISingleton
-class MediaHierarchyManager @Inject constructor(
-    private val context: Context,
-    private val statusBarStateController: SysuiStatusBarStateController,
-    private val keyguardStateController: KeyguardStateController,
-    private val bypassController: KeyguardBypassController,
-    private val mediaCarouselController: MediaCarouselController,
-    private val keyguardViewController: KeyguardViewController,
-    private val dreamOverlayStateController: DreamOverlayStateController,
-    configurationController: ConfigurationController,
-    wakefulnessLifecycle: WakefulnessLifecycle,
-    panelEventsEvents: NotifPanelEvents,
-    private val secureSettings: SecureSettings,
-    @Main private val handler: Handler,
-) {
-
-    /**
-     * Track the media player setting status on lock screen.
-     */
-    private var allowMediaPlayerOnLockScreen: Boolean = true
-    private val lockScreenMediaPlayerUri =
-            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
-
-    /**
-     * Whether we "skip" QQS during panel expansion.
-     *
-     * This means that when expanding the panel we go directly to QS. Also when we are on QS and
-     * start closing the panel, it fully collapses instead of going to QQS.
-     */
-    private var skipQqsOnExpansion: Boolean = false
-
-    /**
-     * The root overlay of the hierarchy. This is where the media notification is attached to
-     * whenever the view is transitioning from one host to another. It also make sure that the
-     * view is always in its final state when it is attached to a view host.
-     */
-    private var rootOverlay: ViewGroupOverlay? = null
-
-    private var rootView: View? = null
-    private var currentBounds = Rect()
-    private var animationStartBounds: Rect = Rect()
-
-    private var animationStartClipping = Rect()
-    private var currentClipping = Rect()
-    private var targetClipping = Rect()
-
-    /**
-     * The cross fade progress at the start of the animation. 0.5f means it's just switching between
-     * the start and the end location and the content is fully faded, while 0.75f means that we're
-     * halfway faded in again in the target state.
-     */
-    private var animationStartCrossFadeProgress = 0.0f
-
-    /**
-     * The starting alpha of the animation
-     */
-    private var animationStartAlpha = 0.0f
-
-    /**
-     * The starting location of the cross fade if an animation is running right now.
-     */
-    @MediaLocation
-    private var crossFadeAnimationStartLocation = -1
-
-    /**
-     * The end location of the cross fade if an animation is running right now.
-     */
-    @MediaLocation
-    private var crossFadeAnimationEndLocation = -1
-    private var targetBounds: Rect = Rect()
-    private val mediaFrame
-        get() = mediaCarouselController.mediaFrame
-    private var statusbarState: Int = statusBarStateController.state
-    private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
-        interpolator = Interpolators.FAST_OUT_SLOW_IN
-        addUpdateListener {
-            updateTargetState()
-            val currentAlpha: Float
-            var boundsProgress = animatedFraction
-            if (isCrossFadeAnimatorRunning) {
-                animationCrossFadeProgress = MathUtils.lerp(animationStartCrossFadeProgress, 1.0f,
-                    animatedFraction)
-                // When crossfading, let's keep the bounds at the right location during fading
-                boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
-                currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress)
-            } else {
-                // If we're not crossfading, let's interpolate from the start alpha to 1.0f
-                currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
-            }
-            interpolateBounds(animationStartBounds, targetBounds, boundsProgress,
-                    result = currentBounds)
-            resolveClipping(currentClipping)
-            applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
-        }
-        addListener(object : AnimatorListenerAdapter() {
-            private var cancelled: Boolean = false
-
-            override fun onAnimationCancel(animation: Animator?) {
-                cancelled = true
-                animationPending = false
-                rootView?.removeCallbacks(startAnimation)
-            }
-
-            override fun onAnimationEnd(animation: Animator?) {
-                isCrossFadeAnimatorRunning = false
-                if (!cancelled) {
-                    applyTargetStateIfNotAnimating()
-                }
-            }
-
-            override fun onAnimationStart(animation: Animator?) {
-                cancelled = false
-                animationPending = false
-            }
-        })
-    }
-
-    private fun resolveClipping(result: Rect) {
-        if (animationStartClipping.isEmpty) result.set(targetClipping)
-        else if (targetClipping.isEmpty) result.set(animationStartClipping)
-        else result.setIntersect(animationStartClipping, targetClipping)
-    }
-
-    private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1)
-    /**
-     * The last location where this view was at before going to the desired location. This is
-     * useful for guided transitions.
-     */
-    @MediaLocation
-    private var previousLocation = -1
-    /**
-     * The desired location where the view will be at the end of the transition.
-     */
-    @MediaLocation
-    private var desiredLocation = -1
-
-    /**
-     * The current attachment location where the view is currently attached.
-     * Usually this matches the desired location except for animations whenever a view moves
-     * to the new desired location, during which it is in [IN_OVERLAY].
-     */
-    @MediaLocation
-    private var currentAttachmentLocation = -1
-
-    private var inSplitShade = false
-
-    /**
-     * Is there any active media in the carousel?
-     */
-    private var hasActiveMedia: Boolean = false
-        get() = mediaHosts.get(LOCATION_QQS)?.visible == true
-
-    /**
-     * Are we currently waiting on an animation to start?
-     */
-    private var animationPending: Boolean = false
-    private val startAnimation: Runnable = Runnable { animator.start() }
-
-    /**
-     * The expansion of quick settings
-     */
-    var qsExpansion: Float = 0.0f
-        set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation()
-                if (getQSTransformationProgress() >= 0) {
-                    updateTargetState()
-                    applyTargetStateIfNotAnimating()
-                }
-            }
-        }
-
-    /**
-     * Is quick setting expanded?
-     */
-    var qsExpanded: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
-            }
-            // qs is expanded on LS shade and HS shade
-            if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
-                mediaCarouselController.logSmartspaceImpression(value)
-            }
-            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-        }
-
-    /**
-     * distance that the full shade transition takes in order for media to fully transition to the
-     * shade
-     */
-    private var distanceForFullShadeTransition = 0
-
-    /**
-     * The amount of progress we are currently in if we're transitioning to the full shade.
-     * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full
-     * shade.
-     */
-    private var fullShadeTransitionProgress = 0f
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
-                // No need to do all the calculations / updates below if we're not on the lockscreen
-                // or if we're bypassing.
-                return
-            }
-            updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
-            if (value >= 0) {
-                updateTargetState()
-                // Setting the alpha directly, as the below call will use it to update the alpha
-                carouselAlpha = calculateAlphaFromCrossFade(field)
-                applyTargetStateIfNotAnimating()
-            }
-        }
-
-    /**
-     * Is there currently a cross-fade animation running driven by an animator?
-     */
-    private var isCrossFadeAnimatorRunning = false
-
-    /**
-     * Are we currently transitionioning from the lockscreen to the full shade
-     * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
-     * the transition starts, this will no longer return true.
-     */
-    private val isTransitioningToFullShade: Boolean
-        get() = fullShadeTransitionProgress != 0f && !bypassController.bypassEnabled &&
-            statusbarState == StatusBarState.KEYGUARD
-
-    /**
-     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
-     * shade. 0.0f means we're not transitioning yet.
-     */
-    fun setTransitionToFullShadeAmount(value: Float) {
-        // If we're transitioning starting on the shade_locked, we don't want any delay and rather
-        // have it aligned with the rest of the animation
-        val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
-        fullShadeTransitionProgress = progress
-    }
-
-    /**
-     * Returns the amount of translationY of the media container, during the current guided
-     * transformation, if running. If there is no guided transformation running, it will return 0.
-     */
-    fun getGuidedTransformationTranslationY(): Int {
-        if (!isCurrentlyInGuidedTransformation()) {
-            return -1
-        }
-        val startHost = getHost(previousLocation) ?: return 0
-        return targetBounds.top - startHost.currentBounds.top
-    }
-
-    /**
-     * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
-     * we wouldn't want to transition in that case.
-     */
-    var collapsingShadeFromQS: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
-        }
-
-    /**
-     * Are location changes currently blocked?
-     */
-    private val blockLocationChanges: Boolean
-        get() {
-            return goingToSleep || dozeAnimationRunning
-        }
-
-    /**
-     * Are we currently going to sleep
-     */
-    private var goingToSleep: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                if (!value) {
-                    updateDesiredLocation()
-                }
-            }
-        }
-
-    /**
-     * Are we currently fullyAwake
-     */
-    private var fullyAwake: Boolean = false
-        set(value) {
-            if (field != value) {
-                field = value
-                if (value) {
-                    updateDesiredLocation(forceNoAnimation = true)
-                }
-            }
-        }
-
-    /**
-     * Is the doze animation currently Running
-     */
-    private var dozeAnimationRunning: Boolean = false
-        private set(value) {
-            if (field != value) {
-                field = value
-                if (!value) {
-                    updateDesiredLocation()
-                }
-            }
-        }
-
-    /**
-     * Is the dream overlay currently active
-     */
-    private var dreamOverlayActive: Boolean = false
-        private set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
-        }
-
-    /**
-     * Is the dream media complication currently active
-     */
-    private var dreamMediaComplicationActive: Boolean = false
-        private set(value) {
-            if (field != value) {
-                field = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
-        }
-
-    /**
-     * The current cross fade progress. 0.5f means it's just switching
-     * between the start and the end location and the content is fully faded, while 0.75f means
-     * that we're halfway faded in again in the target state.
-     * This is only valid while [isCrossFadeAnimatorRunning] is true.
-     */
-    private var animationCrossFadeProgress = 1.0f
-
-    /**
-     * The current carousel Alpha.
-     */
-    private var carouselAlpha: Float = 1.0f
-        set(value) {
-            if (field == value) {
-                return
-            }
-            field = value
-            CrossFadeHelper.fadeIn(mediaFrame, value)
-        }
-
-    /**
-     * Calculate the alpha of the view when given a cross-fade progress.
-     *
-     * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
-     * between the start and the end location and the content is fully faded, while 0.75f means
-     * that we're halfway faded in again in the target state.
-     */
-    private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
-        if (crossFadeProgress <= 0.5f) {
-            return 1.0f - crossFadeProgress / 0.5f
-        } else {
-            return (crossFadeProgress - 0.5f) / 0.5f
-        }
-    }
-
-    init {
-        updateConfiguration()
-        configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                updateConfiguration()
-                updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true)
-            }
-        })
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onStatePreChange(oldState: Int, newState: Int) {
-                // We're updating the location before the state change happens, since we want the
-                // location of the previous state to still be up to date when the animation starts
-                statusbarState = newState
-                updateDesiredLocation()
-            }
-
-            override fun onStateChanged(newState: Int) {
-                updateTargetState()
-                // Enters shade from lock screen
-                if (newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()) {
-                    mediaCarouselController.logSmartspaceImpression(qsExpanded)
-                }
-                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-            }
-
-            override fun onDozeAmountChanged(linear: Float, eased: Float) {
-                dozeAnimationRunning = linear != 0.0f && linear != 1.0f
-            }
-
-            override fun onDozingChanged(isDozing: Boolean) {
-                if (!isDozing) {
-                    dozeAnimationRunning = false
-                    // Enters lock screen from screen off
-                    if (isLockScreenVisibleToUser()) {
-                        mediaCarouselController.logSmartspaceImpression(qsExpanded)
-                    }
-                } else {
-                    updateDesiredLocation()
-                    qsExpanded = false
-                    closeGuts()
-                }
-                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-            }
-
-            override fun onExpandedChanged(isExpanded: Boolean) {
-                // Enters shade from home screen
-                if (isHomeScreenShadeVisibleToUser()) {
-                    mediaCarouselController.logSmartspaceImpression(qsExpanded)
-                }
-                mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-            }
-        })
-
-        dreamOverlayStateController.addCallback(object : DreamOverlayStateController.Callback {
-            override fun onComplicationsChanged() {
-                dreamMediaComplicationActive = dreamOverlayStateController.complications.any {
-                    it is MediaDreamComplication
-                }
-            }
-
-            override fun onStateChanged() {
-                dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
-            }
-        })
-
-        wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
-            override fun onFinishedGoingToSleep() {
-                goingToSleep = false
-            }
-
-            override fun onStartedGoingToSleep() {
-                goingToSleep = true
-                fullyAwake = false
-            }
-
-            override fun onFinishedWakingUp() {
-                goingToSleep = false
-                fullyAwake = true
-            }
-
-            override fun onStartedWakingUp() {
-                goingToSleep = false
-            }
-        })
-
-        mediaCarouselController.updateUserVisibility = {
-            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
-        }
-        mediaCarouselController.updateHostVisibility = {
-            mediaHosts.forEach {
-                it?.updateViewVisibility()
-            }
-        }
-
-        panelEventsEvents.registerListener(object : NotifPanelEvents.Listener {
-            override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
-                skipQqsOnExpansion = isExpandImmediateEnabled
-                updateDesiredLocation()
-            }
-        })
-
-        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
-            override fun onChange(selfChange: Boolean, uri: Uri?) {
-                if (uri == lockScreenMediaPlayerUri) {
-                    allowMediaPlayerOnLockScreen =
-                            secureSettings.getBoolForUser(
-                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                                    true,
-                                    UserHandle.USER_CURRENT
-                            )
-                }
-            }
-        }
-        secureSettings.registerContentObserverForUser(
-                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                settingsObserver,
-                UserHandle.USER_ALL)
-    }
-
-    private fun updateConfiguration() {
-        distanceForFullShadeTransition = context.resources.getDimensionPixelSize(
-                R.dimen.lockscreen_shade_media_transition_distance)
-        inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
-    }
-
-    /**
-     * Register a media host and create a view can be attached to a view hierarchy
-     * and where the players will be placed in when the host is the currently desired state.
-     *
-     * @return the hostView associated with this location
-     */
-    fun register(mediaObject: MediaHost): UniqueObjectHostView {
-        val viewHost = createUniqueObjectHost()
-        mediaObject.hostView = viewHost
-        mediaObject.addVisibilityChangeListener {
-            // If QQS changes visibility, we need to force an update to ensure the transition
-            // goes into the correct state
-            val stateUpdate = mediaObject.location == LOCATION_QQS
-
-            // Never animate because of a visibility change, only state changes should do that
-            updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
-        }
-        mediaHosts[mediaObject.location] = mediaObject
-        if (mediaObject.location == desiredLocation) {
-            // In case we are overriding a view that is already visible, make sure we attach it
-            // to this new host view in the below call
-            desiredLocation = -1
-        }
-        if (mediaObject.location == currentAttachmentLocation) {
-            currentAttachmentLocation = -1
-        }
-        updateDesiredLocation()
-        return viewHost
-    }
-
-    /**
-     * Close the guts in all players in [MediaCarouselController].
-     */
-    fun closeGuts() {
-        mediaCarouselController.closeGuts()
-    }
-
-    private fun createUniqueObjectHost(): UniqueObjectHostView {
-        val viewHost = UniqueObjectHostView(context)
-        viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
-            override fun onViewAttachedToWindow(p0: View?) {
-                if (rootOverlay == null) {
-                    rootView = viewHost.viewRootImpl.view
-                    rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
-                }
-                viewHost.removeOnAttachStateChangeListener(this)
-            }
-
-            override fun onViewDetachedFromWindow(p0: View?) {
-            }
-        })
-        return viewHost
-    }
-
-    /**
-     * Updates the location that the view should be in. If it changes, an animation may be triggered
-     * going from the old desired location to the new one.
-     *
-     * @param forceNoAnimation optional parameter telling the system not to animate
-     * @param forceStateUpdate optional parameter telling the system to update transition state
-     *                         even if location did not change
-     */
-    private fun updateDesiredLocation(
-        forceNoAnimation: Boolean = false,
-        forceStateUpdate: Boolean = false
-    ) = traceSection("MediaHierarchyManager#updateDesiredLocation") {
-        val desiredLocation = calculateLocation()
-        if (desiredLocation != this.desiredLocation || forceStateUpdate) {
-            if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
-                // Only update previous location when it actually changes
-                previousLocation = this.desiredLocation
-            } else if (forceStateUpdate) {
-                val onLockscreen = (!bypassController.bypassEnabled &&
-                        (statusbarState == StatusBarState.KEYGUARD))
-                if (desiredLocation == LOCATION_QS && previousLocation == LOCATION_LOCKSCREEN &&
-                        !onLockscreen) {
-                    // If media active state changed and the device is now unlocked, update the
-                    // previous location so we animate between the correct hosts
-                    previousLocation = LOCATION_QQS
-                }
-            }
-            val isNewView = this.desiredLocation == -1
-            this.desiredLocation = desiredLocation
-            // Let's perform a transition
-            val animate = !forceNoAnimation &&
-                    shouldAnimateTransition(desiredLocation, previousLocation)
-            val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
-            val host = getHost(desiredLocation)
-            val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
-            if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
-                // if we're fading, we want the desired location / measurement only to change
-                // once fully faded. This is happening in the host attachment
-                mediaCarouselController.onDesiredLocationChanged(desiredLocation, host,
-                    animate, animDuration, delay)
-            }
-            performTransitionToNewLocation(isNewView, animate)
-        }
-    }
-
-    private fun performTransitionToNewLocation(
-        isNewView: Boolean,
-        animate: Boolean
-    ) = traceSection("MediaHierarchyManager#performTransitionToNewLocation") {
-        if (previousLocation < 0 || isNewView) {
-            cancelAnimationAndApplyDesiredState()
-            return
-        }
-        val currentHost = getHost(desiredLocation)
-        val previousHost = getHost(previousLocation)
-        if (currentHost == null || previousHost == null) {
-            cancelAnimationAndApplyDesiredState()
-            return
-        }
-        updateTargetState()
-        if (isCurrentlyInGuidedTransformation()) {
-            applyTargetStateIfNotAnimating()
-        } else if (animate) {
-            val wasCrossFading = isCrossFadeAnimatorRunning
-            val previewsCrossFadeProgress = animationCrossFadeProgress
-            animator.cancel()
-            if (currentAttachmentLocation != previousLocation ||
-                    !previousHost.hostView.isAttachedToWindow) {
-                // Let's animate to the new position, starting from the current position
-                // We also go in here in case the view was detached, since the bounds wouldn't
-                // be correct anymore
-                animationStartBounds.set(currentBounds)
-                animationStartClipping.set(currentClipping)
-            } else {
-                // otherwise, let's take the freshest state, since the current one could
-                // be outdated
-                animationStartBounds.set(previousHost.currentBounds)
-                animationStartClipping.set(previousHost.currentClipping)
-            }
-            val transformationType = calculateTransformationType()
-            var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
-            var crossFadeStartProgress = 0.0f
-            // The alpha is only relevant when not cross fading
-            var newCrossFadeStartLocation = previousLocation
-            if (wasCrossFading) {
-                if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
-                    if (needsCrossFade) {
-                        // We were previously crossFading and we've already reached
-                        // the end view, Let's start crossfading from the same position there
-                        crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
-                    }
-                    // Otherwise let's fade in from the current alpha, but not cross fade
-                } else {
-                    // We haven't reached the previous location yet, let's still cross fade from
-                    // where we were.
-                    newCrossFadeStartLocation = crossFadeAnimationStartLocation
-                    if (newCrossFadeStartLocation == desiredLocation) {
-                        // we're crossFading back to where we were, let's start at the end position
-                        crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
-                    } else {
-                        // Let's start from where we are right now
-                        crossFadeStartProgress = previewsCrossFadeProgress
-                        // We need to force cross fading as we haven't reached the end location yet
-                        needsCrossFade = true
-                    }
-                }
-            } else if (needsCrossFade) {
-                // let's not flicker and start with the same alpha
-                crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
-            }
-            isCrossFadeAnimatorRunning = needsCrossFade
-            crossFadeAnimationStartLocation = newCrossFadeStartLocation
-            crossFadeAnimationEndLocation = desiredLocation
-            animationStartAlpha = carouselAlpha
-            animationStartCrossFadeProgress = crossFadeStartProgress
-            adjustAnimatorForTransition(desiredLocation, previousLocation)
-            if (!animationPending) {
-                rootView?.let {
-                    // Let's delay the animation start until we finished laying out
-                    animationPending = true
-                    it.postOnAnimation(startAnimation)
-                }
-            }
-        } else {
-            cancelAnimationAndApplyDesiredState()
-        }
-    }
-
-    private fun shouldAnimateTransition(
-        @MediaLocation currentLocation: Int,
-        @MediaLocation previousLocation: Int
-    ): Boolean {
-        if (isCurrentlyInGuidedTransformation()) {
-            return false
-        }
-        if (skipQqsOnExpansion) {
-            return false
-        }
-        // This is an invalid transition, and can happen when using the camera gesture from the
-        // lock screen. Disallow.
-        if (previousLocation == LOCATION_LOCKSCREEN &&
-            desiredLocation == LOCATION_QQS &&
-            statusbarState == StatusBarState.SHADE) {
-            return false
-        }
-
-        if (currentLocation == LOCATION_QQS &&
-                previousLocation == LOCATION_LOCKSCREEN &&
-                (statusBarStateController.leaveOpenOnKeyguardHide() ||
-                        statusbarState == StatusBarState.SHADE_LOCKED)) {
-            // Usually listening to the isShown is enough to determine this, but there is some
-            // non-trivial reattaching logic happening that will make the view not-shown earlier
-            return true
-        }
-
-        if (statusbarState == StatusBarState.KEYGUARD && (currentLocation == LOCATION_LOCKSCREEN ||
-                        previousLocation == LOCATION_LOCKSCREEN)) {
-            // We're always fading from lockscreen to keyguard in situations where the player
-            // is already fully hidden
-            return false
-        }
-        return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
-    }
-
-    private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
-        val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
-        animator.apply {
-            duration = animDuration
-            startDelay = delay
-        }
-    }
-
-    private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
-        var animDuration = 200L
-        var delay = 0L
-        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
-            // Going to the full shade, let's adjust the animation duration
-            if (statusbarState == StatusBarState.SHADE &&
-                    keyguardStateController.isKeyguardFadingAway) {
-                delay = keyguardStateController.keyguardFadingAwayDelay
-            }
-            animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
-        } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
-            animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
-        }
-        return animDuration to delay
-    }
-
-    private fun applyTargetStateIfNotAnimating() {
-        if (!animator.isRunning) {
-            // Let's immediately apply the target state (which is interpolated) if there is
-            // no animation running. Otherwise the animation update will already update
-            // the location
-            applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
-        }
-    }
-
-    /**
-     * Updates the bounds that the view wants to be in at the end of the animation.
-     */
-    private fun updateTargetState() {
-        var starthost = getHost(previousLocation)
-        var endHost = getHost(desiredLocation)
-        if (isCurrentlyInGuidedTransformation() && !isCurrentlyFading() && starthost != null &&
-            endHost != null) {
-            val progress = getTransformationProgress()
-            // If either of the hosts are invisible, let's keep them at the other host location to
-            // have a nicer disappear animation. Otherwise the currentBounds of the state might
-            // be undefined
-            if (!endHost.visible) {
-                endHost = starthost
-            } else if (!starthost.visible) {
-                starthost = endHost
-            }
-            val newBounds = endHost.currentBounds
-            val previousBounds = starthost.currentBounds
-            targetBounds = interpolateBounds(previousBounds, newBounds, progress)
-            targetClipping = endHost.currentClipping
-        } else if (endHost != null) {
-            val bounds = endHost.currentBounds
-            targetBounds.set(bounds)
-            targetClipping = endHost.currentClipping
-        }
-    }
-
-    private fun interpolateBounds(
-        startBounds: Rect,
-        endBounds: Rect,
-        progress: Float,
-        result: Rect? = null
-    ): Rect {
-        val left = MathUtils.lerp(startBounds.left.toFloat(),
-                endBounds.left.toFloat(), progress).toInt()
-        val top = MathUtils.lerp(startBounds.top.toFloat(),
-                endBounds.top.toFloat(), progress).toInt()
-        val right = MathUtils.lerp(startBounds.right.toFloat(),
-                endBounds.right.toFloat(), progress).toInt()
-        val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
-                endBounds.bottom.toFloat(), progress).toInt()
-        val resultBounds = result ?: Rect()
-        resultBounds.set(left, top, right, bottom)
-        return resultBounds
-    }
-
-    /** @return true if this transformation is guided by an external progress like a finger */
-    fun isCurrentlyInGuidedTransformation(): Boolean {
-        return hasValidStartAndEndLocations() &&
-                getTransformationProgress() >= 0 &&
-                areGuidedTransitionHostsVisible()
-    }
-
-    private fun hasValidStartAndEndLocations(): Boolean {
-        return previousLocation != -1 && desiredLocation != -1
-    }
-
-    /**
-     * Calculate the transformation type for the current animation
-     */
-    @VisibleForTesting
-    @TransformationType
-    fun calculateTransformationType(): Int {
-        if (isTransitioningToFullShade) {
-            if (inSplitShade && areGuidedTransitionHostsVisible()) {
-                return TRANSFORMATION_TYPE_TRANSITION
-            }
-            return TRANSFORMATION_TYPE_FADE
-        }
-        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
-            previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN) {
-            // animating between ls and qs should fade, as QS is clipped.
-            return TRANSFORMATION_TYPE_FADE
-        }
-        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
-            // animating between ls and qqs should fade when dragging down via e.g. expand button
-            return TRANSFORMATION_TYPE_FADE
-        }
-        return TRANSFORMATION_TYPE_TRANSITION
-    }
-
-    private fun areGuidedTransitionHostsVisible(): Boolean {
-        return getHost(previousLocation)?.visible == true &&
-                getHost(desiredLocation)?.visible == true
-    }
-
-    /**
-     * @return the current transformation progress if we're in a guided transformation and -1
-     * otherwise
-     */
-    private fun getTransformationProgress(): Float {
-        if (skipQqsOnExpansion) {
-            return -1.0f
-        }
-        val progress = getQSTransformationProgress()
-        if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
-            return progress
-        }
-        if (isTransitioningToFullShade) {
-            return fullShadeTransitionProgress
-        }
-        return -1.0f
-    }
-
-    private fun getQSTransformationProgress(): Float {
-        val currentHost = getHost(desiredLocation)
-        val previousHost = getHost(previousLocation)
-        if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
-            if (previousHost?.location == LOCATION_QQS) {
-                if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
-                    return qsExpansion
-                }
-            }
-        }
-        return -1.0f
-    }
-
-    private fun getHost(@MediaLocation location: Int): MediaHost? {
-        if (location < 0) {
-            return null
-        }
-        return mediaHosts[location]
-    }
-
-    private fun cancelAnimationAndApplyDesiredState() {
-        animator.cancel()
-        getHost(desiredLocation)?.let {
-            applyState(it.currentBounds, alpha = 1.0f, immediately = true)
-        }
-    }
-
-    /**
-     * Apply the current state to the view, updating it's bounds and desired state
-     */
-    private fun applyState(
-        bounds: Rect,
-        alpha: Float,
-        immediately: Boolean = false,
-        clipBounds: Rect = EMPTY_RECT
-    ) = traceSection("MediaHierarchyManager#applyState") {
-        currentBounds.set(bounds)
-        currentClipping = clipBounds
-        carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
-        val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
-        val startLocation = if (onlyUseEndState) -1 else previousLocation
-        val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
-        val endLocation = resolveLocationForFading()
-        mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
-        updateHostAttachment()
-        if (currentAttachmentLocation == IN_OVERLAY) {
-            // Setting the clipping on the hierarchy of `mediaFrame` does not work
-            if (!currentClipping.isEmpty) {
-                currentBounds.intersect(currentClipping)
-            }
-            mediaFrame.setLeftTopRightBottom(
-                    currentBounds.left,
-                    currentBounds.top,
-                    currentBounds.right,
-                    currentBounds.bottom)
-        }
-    }
-
-    private fun updateHostAttachment() = traceSection(
-        "MediaHierarchyManager#updateHostAttachment"
-    ) {
-        var newLocation = resolveLocationForFading()
-        var canUseOverlay = !isCurrentlyFading()
-        if (isCrossFadeAnimatorRunning) {
-            if (getHost(newLocation)?.visible == true &&
-                getHost(newLocation)?.hostView?.isShown == false &&
-                newLocation != desiredLocation) {
-                // We're crossfading but the view is already hidden. Let's move to the overlay
-                // instead. This happens when animating to the full shade using a button click.
-                canUseOverlay = true
-            }
-        }
-        val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
-        newLocation = if (inOverlay) IN_OVERLAY else newLocation
-        if (currentAttachmentLocation != newLocation) {
-            currentAttachmentLocation = newLocation
-
-            // Remove the carousel from the old host
-            (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
-
-            // Add it to the new one
-            if (inOverlay) {
-                rootOverlay!!.add(mediaFrame)
-            } else {
-                val targetHost = getHost(newLocation)!!.hostView
-                // When adding back to the host, let's make sure to reset the bounds.
-                // Usually adding the view will trigger a layout that does this automatically,
-                // but we sometimes suppress this.
-                targetHost.addView(mediaFrame)
-                val left = targetHost.paddingLeft
-                val top = targetHost.paddingTop
-                mediaFrame.setLeftTopRightBottom(
-                        left,
-                        top,
-                        left + currentBounds.width(),
-                        top + currentBounds.height())
-
-                if (mediaFrame.childCount > 0) {
-                    val child = mediaFrame.getChildAt(0)
-                    if (mediaFrame.height < child.height) {
-                        Log.wtf(TAG, "mediaFrame height is too small for child: " +
-                            "${mediaFrame.height} vs ${child.height}")
-                    }
-                }
-            }
-            if (isCrossFadeAnimatorRunning) {
-                // When cross-fading with an animation, we only notify the media carousel of the
-                // location change, once the view is reattached to the new place and not immediately
-                // when the desired location changes. This callback will update the measurement
-                // of the carousel, only once we've faded out at the old location and then reattach
-                // to fade it in at the new location.
-                mediaCarouselController.onDesiredLocationChanged(
-                    newLocation,
-                    getHost(newLocation),
-                    animate = false
-                )
-            }
-        }
-    }
-
-    /**
-     * Calculate the location when cross fading between locations. While fading out,
-     * the content should remain in the previous location, while after the switch it should
-     * be at the desired location.
-     */
-    private fun resolveLocationForFading(): Int {
-        if (isCrossFadeAnimatorRunning) {
-            // When animating between two hosts with a fade, let's keep ourselves in the old
-            // location for the first half, and then switch over to the end location
-            if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
-                return crossFadeAnimationEndLocation
-            } else {
-                return crossFadeAnimationStartLocation
-            }
-        }
-        return desiredLocation
-    }
-
-    private fun isTransitionRunning(): Boolean {
-        return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
-                animator.isRunning || animationPending
-    }
-
-    @MediaLocation
-    private fun calculateLocation(): Int {
-        if (blockLocationChanges) {
-            // Keep the current location until we're allowed to again
-            return desiredLocation
-        }
-        val onLockscreen = (!bypassController.bypassEnabled &&
-            (statusbarState == StatusBarState.KEYGUARD))
-        val location = when {
-            dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
-            (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
-            qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
-            !hasActiveMedia -> LOCATION_QS
-            onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
-            onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-            onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
-            else -> LOCATION_QQS
-        }
-        // When we're on lock screen and the player is not active, we should keep it in QS.
-        // Otherwise it will try to animate a transition that doesn't make sense.
-        if (location == LOCATION_LOCKSCREEN && getHost(location)?.visible != true &&
-            !statusBarStateController.isDozing) {
-            return LOCATION_QS
-        }
-        if (location == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS &&
-            collapsingShadeFromQS) {
-            // When collapsing on the lockscreen, we want to remain in QS
-            return LOCATION_QS
-        }
-        if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN &&
-            !fullyAwake) {
-            // When unlocking from dozing / while waking up, the media shouldn't be transitioning
-            // in an animated way. Let's keep it in the lockscreen until we're fully awake and
-            // reattach it without an animation
-            return LOCATION_LOCKSCREEN
-        }
-        if (skipQqsOnExpansion) {
-            // When doing an immediate expand or collapse, we want to keep it in QS.
-            return LOCATION_QS
-        }
-        return location
-    }
-
-    private fun isSplitShadeExpanding(): Boolean {
-        return inSplitShade && isTransitioningToFullShade
-    }
-
-    /**
-     * Are we currently transforming to the full shade and already in QQS
-     */
-    private fun isTransformingToFullShadeAndInQQS(): Boolean {
-        if (!isTransitioningToFullShade) {
-            return false
-        }
-        if (inSplitShade) {
-            // Split shade doesn't use QQS.
-            return false
-        }
-        return fullShadeTransitionProgress > 0.5f
-    }
-
-    /**
-     * Is the current transformationType fading
-     */
-    private fun isCurrentlyFading(): Boolean {
-        if (isSplitShadeExpanding()) {
-            // Split shade always uses transition instead of fade.
-            return false
-        }
-        if (isTransitioningToFullShade) {
-            return true
-        }
-        return isCrossFadeAnimatorRunning
-    }
-
-    /**
-     * Returns true when the media card could be visible to the user if existed.
-     */
-    private fun isVisibleToUser(): Boolean {
-        return isLockScreenVisibleToUser() || isLockScreenShadeVisibleToUser() ||
-                isHomeScreenShadeVisibleToUser()
-    }
-
-    private fun isLockScreenVisibleToUser(): Boolean {
-        return !statusBarStateController.isDozing &&
-                !keyguardViewController.isBouncerShowing &&
-                statusBarStateController.state == StatusBarState.KEYGUARD &&
-                allowMediaPlayerOnLockScreen &&
-                statusBarStateController.isExpanded &&
-                !qsExpanded
-    }
-
-    private fun isLockScreenShadeVisibleToUser(): Boolean {
-        return !statusBarStateController.isDozing &&
-                !keyguardViewController.isBouncerShowing &&
-                (statusBarStateController.state == StatusBarState.SHADE_LOCKED ||
-                        (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded))
-    }
-
-    private fun isHomeScreenShadeVisibleToUser(): Boolean {
-        return !statusBarStateController.isDozing &&
-                statusBarStateController.state == StatusBarState.SHADE &&
-                statusBarStateController.isExpanded
-    }
-
-    companion object {
-        /**
-         * Attached in expanded quick settings
-         */
-        const val LOCATION_QS = 0
-
-        /**
-         * Attached in the collapsed QS
-         */
-        const val LOCATION_QQS = 1
-
-        /**
-         * Attached on the lock screen
-         */
-        const val LOCATION_LOCKSCREEN = 2
-
-        /**
-         * Attached on the dream overlay
-         */
-        const val LOCATION_DREAM_OVERLAY = 3
-
-        /**
-         * Attached at the root of the hierarchy in an overlay
-         */
-        const val IN_OVERLAY = -1000
-
-        /**
-         * The default transformation type where the hosts transform into each other using a direct
-         * transition
-         */
-        const val TRANSFORMATION_TYPE_TRANSITION = 0
-
-        /**
-         * A transformation type where content fades from one place to another instead of
-         * transitioning
-         */
-        const val TRANSFORMATION_TYPE_FADE = 1
-    }
-}
-private val EMPTY_RECT = Rect()
-
-@IntDef(prefix = ["TRANSFORMATION_TYPE_"], value = [
-    MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
-    MediaHierarchyManager.TRANSFORMATION_TYPE_FADE])
-@Retention(AnnotationRetention.SOURCE)
-private annotation class TransformationType
-
-@IntDef(prefix = ["LOCATION_"], value = [
-    MediaHierarchyManager.LOCATION_QS,
-    MediaHierarchyManager.LOCATION_QQS,
-    MediaHierarchyManager.LOCATION_LOCKSCREEN,
-    MediaHierarchyManager.LOCATION_DREAM_OVERLAY])
-@Retention(AnnotationRetention.SOURCE)
-annotation class MediaLocation
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
deleted file mode 100644
index aea2934..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-/**
- * A class responsible for managing all media host states of the various host locations and
- * coordinating the heights among different players. This class can be used to get the most up to
- * date state for any location.
- */
-@SysUISingleton
-class MediaHostStatesManager @Inject constructor() {
-
-    private val callbacks: MutableSet<Callback> = mutableSetOf()
-    private val controllers: MutableSet<MediaViewController> = mutableSetOf()
-
-    /**
-     * The overall sizes of the carousel. This is needed to make sure all players in the carousel
-     * have equal size.
-     */
-    val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
-
-    /**
-     * A map with all media states of all locations.
-     */
-    val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
-
-    /**
-     * Notify that a media state for a given location has changed. Should only be called from
-     * Media hosts themselves.
-     */
-    fun updateHostState(
-        @MediaLocation location: Int,
-        hostState: MediaHostState
-    ) = traceSection("MediaHostStatesManager#updateHostState") {
-        val currentState = mediaHostStates.get(location)
-        if (!hostState.equals(currentState)) {
-            val newState = hostState.copy()
-            mediaHostStates.put(location, newState)
-            updateCarouselDimensions(location, hostState)
-            // First update all the controllers to ensure they get the chance to measure
-            for (controller in controllers) {
-                controller.stateCallback.onHostStateChanged(location, newState)
-            }
-
-            // Then update all other callbacks which may depend on the controllers above
-            for (callback in callbacks) {
-                callback.onHostStateChanged(location, newState)
-            }
-        }
-    }
-
-    /**
-     * Get the dimensions of all players combined, which determines the overall height of the
-     * media carousel and the media hosts.
-     */
-    fun updateCarouselDimensions(
-        @MediaLocation location: Int,
-        hostState: MediaHostState
-    ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") {
-        val result = MeasurementOutput(0, 0)
-        for (controller in controllers) {
-            val measurement = controller.getMeasurementsForState(hostState)
-            measurement?.let {
-                if (it.measuredHeight > result.measuredHeight) {
-                    result.measuredHeight = it.measuredHeight
-                }
-                if (it.measuredWidth > result.measuredWidth) {
-                    result.measuredWidth = it.measuredWidth
-                }
-            }
-        }
-        carouselSizes[location] = result
-        return result
-    }
-
-    /**
-     * Add a callback to be called when a MediaState has updated
-     */
-    fun addCallback(callback: Callback) {
-        callbacks.add(callback)
-    }
-
-    /**
-     * Remove a callback that listens to media states
-     */
-    fun removeCallback(callback: Callback) {
-        callbacks.remove(callback)
-    }
-
-    /**
-     * Register a controller that listens to media states and is used to determine the size of
-     * the media carousel
-     */
-    fun addController(controller: MediaViewController) {
-        controllers.add(controller)
-    }
-
-    /**
-     * Notify the manager about the removal of a controller.
-     */
-    fun removeController(controller: MediaViewController) {
-        controllers.remove(controller)
-    }
-
-    interface Callback {
-        /**
-         * Notify the callbacks that a media state for a host has changed, and that the
-         * corresponding view states should be updated and applied
-         */
-        fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 1ac2a07..be357ee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -182,8 +182,7 @@
 
     override fun shouldGetOnlyDefaultActivities() = false
 
-    // TODO(b/240924732) flip the flag when the recents selector is ready
-    override fun shouldShowContentPreview() = false
+    override fun shouldShowContentPreview() = true
 
     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
         recentsViewController.createView(parent)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
deleted file mode 100644
index 00273bc..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt
+++ /dev/null
@@ -1,131 +0,0 @@
-package com.android.systemui.media
-
-import android.content.Context
-import android.os.SystemClock
-import android.util.AttributeSet
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewGroup
-import android.widget.HorizontalScrollView
-import com.android.systemui.Gefingerpoken
-import com.android.wm.shell.animation.physicsAnimator
-
-/**
- * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful
- * when only measuring children but not the parent, when trying to apply a new scroll position
- */
-class MediaScrollView @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttr: Int = 0
-)
-    : HorizontalScrollView(context, attrs, defStyleAttr) {
-
-    lateinit var contentContainer: ViewGroup
-        private set
-    var touchListener: Gefingerpoken? = null
-
-    /**
-     * The target value of the translation X animation. Only valid if the physicsAnimator is running
-     */
-    var animationTargetX = 0.0f
-
-    /**
-     * Get the current content translation. This is usually the normal translationX of the content,
-     * but when animating, it might differ
-     */
-    fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) {
-        animationTargetX
-    } else {
-        contentContainer.translationX
-    }
-
-    /**
-     * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
-     * carousel.  The player indices are always relative (start-to-end) and the scrollView.scrollX
-     * is always absolute.  This function is its own inverse.
-     */
-    private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) {
-        contentContainer.width - width - scrollX
-    } else {
-        scrollX
-    }
-
-    /**
-     * Get the layoutDirection-relative (start-to-end) scroll X position of the carousel.
-     */
-    var relativeScrollX: Int
-        get() = transformScrollX(scrollX)
-        set(value) {
-            scrollX = transformScrollX(value)
-        }
-
-    /**
-     * Allow all scrolls to go through, use base implementation
-     */
-    override fun scrollTo(x: Int, y: Int) {
-        if (mScrollX != x || mScrollY != y) {
-            val oldX: Int = mScrollX
-            val oldY: Int = mScrollY
-            mScrollX = x
-            mScrollY = y
-            invalidateParentCaches()
-            onScrollChanged(mScrollX, mScrollY, oldX, oldY)
-            if (!awakenScrollBars()) {
-                postInvalidateOnAnimation()
-            }
-        }
-    }
-
-    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
-        var intercept = false
-        touchListener?.let {
-            intercept = it.onInterceptTouchEvent(ev)
-        }
-        return super.onInterceptTouchEvent(ev) || intercept
-    }
-
-    override fun onTouchEvent(ev: MotionEvent?): Boolean {
-        var touch = false
-        touchListener?.let {
-            touch = it.onTouchEvent(ev)
-        }
-        return super.onTouchEvent(ev) || touch
-    }
-
-    override fun onFinishInflate() {
-        super.onFinishInflate()
-        contentContainer = getChildAt(0) as ViewGroup
-    }
-
-    override fun overScrollBy(
-        deltaX: Int,
-        deltaY: Int,
-        scrollX: Int,
-        scrollY: Int,
-        scrollRangeX: Int,
-        scrollRangeY: Int,
-        maxOverScrollX: Int,
-        maxOverScrollY: Int,
-        isTouchEvent: Boolean
-    ): Boolean {
-        if (getContentTranslation() != 0.0f) {
-            // When we're dismissing we ignore all the scrolling
-            return false
-        }
-        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
-                scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
-    }
-
-    /**
-     * Cancel the current touch event going on.
-     */
-    fun cancelCurrentScroll() {
-        val now = SystemClock.uptimeMillis()
-        val event = MotionEvent.obtain(now, now,
-                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
-        event.source = InputDevice.SOURCE_TOUCHSCREEN
-        super.onTouchEvent(event)
-        event.recycle()
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
deleted file mode 100644
index 8c9e2d8..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutLogger.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.media.session.PlaybackState
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.dagger.MediaTimeoutListenerLog
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import javax.inject.Inject
-private const val TAG = "MediaTimeout"
-
-/**
- * A buffered log for [MediaTimeoutListener] events
- */
-@SysUISingleton
-class MediaTimeoutLogger @Inject constructor(
-    @MediaTimeoutListenerLog private val buffer: LogBuffer
-) {
-    fun logReuseListener(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "reuse listener: $str1"
-        }
-    )
-
-    fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = oldKey
-            str2 = newKey
-            bool1 = hadListener
-        },
-        {
-            "migrate from $str1 to $str2, had listener? $bool1"
-        }
-    )
-
-    fun logUpdateListener(key: String, wasPlaying: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-            bool1 = wasPlaying
-        },
-        {
-            "updating $str1, was playing? $bool1"
-        }
-    )
-
-    fun logDelayedUpdate(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "deliver delayed playback state for $str1"
-        }
-    )
-
-    fun logSessionDestroyed(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "session destroyed $str1"
-        }
-    )
-
-    fun logPlaybackState(key: String, state: PlaybackState?) = buffer.log(
-        TAG,
-        LogLevel.VERBOSE,
-        {
-            str1 = key
-            str2 = state?.toString()
-        },
-        {
-            "state update: key=$str1 state=$str2"
-        }
-    )
-
-    fun logStateCallback(key: String) = buffer.log(
-            TAG,
-            LogLevel.VERBOSE,
-            {
-                str1 = key
-            },
-            {
-                "dispatching state update for $key"
-            }
-    )
-
-    fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-            bool1 = playing
-            bool2 = resumption
-        },
-        {
-            "schedule timeout $str1, playing=$bool1 resumption=$bool2"
-        }
-    )
-
-    fun logCancelIgnored(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "cancellation already exists for $str1"
-        }
-    )
-
-    fun logTimeout(key: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = key
-        },
-        {
-            "execute timeout for $str1"
-        }
-    )
-
-    fun logTimeoutCancelled(key: String, reason: String) = buffer.log(
-        TAG,
-        LogLevel.VERBOSE,
-        {
-            str1 = key
-            str2 = reason
-        },
-        {
-            "media timeout cancelled for $str1, reason: $str2"
-        }
-    )
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
deleted file mode 100644
index faa7aae..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ /dev/null
@@ -1,615 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.media
-
-import android.content.Context
-import android.content.res.Configuration
-import androidx.annotation.VisibleForTesting
-import androidx.constraintlayout.widget.ConstraintSet
-import com.android.systemui.R
-import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.calculateAlpha
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.animation.MeasurementOutput
-import com.android.systemui.util.animation.TransitionLayout
-import com.android.systemui.util.animation.TransitionLayoutController
-import com.android.systemui.util.animation.TransitionViewState
-import com.android.systemui.util.traceSection
-import javax.inject.Inject
-
-/**
- * A class responsible for controlling a single instance of a media player handling interactions
- * with the view instance and keeping the media view states up to date.
- */
-class MediaViewController @Inject constructor(
-    private val context: Context,
-    private val configurationController: ConfigurationController,
-    private val mediaHostStatesManager: MediaHostStatesManager,
-    private val logger: MediaViewLogger
-) {
-
-    /**
-     * Indicating that the media view controller is for a notification-based player,
-     * session-based player, or recommendation
-     */
-    enum class TYPE {
-        PLAYER, RECOMMENDATION
-    }
-
-    companion object {
-        @JvmField
-        val GUTS_ANIMATION_DURATION = 500L
-        val controlIds = setOf(
-                R.id.media_progress_bar,
-                R.id.actionNext,
-                R.id.actionPrev,
-                R.id.action0,
-                R.id.action1,
-                R.id.action2,
-                R.id.action3,
-                R.id.action4,
-                R.id.media_scrubbing_elapsed_time,
-                R.id.media_scrubbing_total_time
-        )
-
-        val detailIds = setOf(
-                R.id.header_title,
-                R.id.header_artist,
-                R.id.actionPlayPause,
-        )
-    }
-
-    /**
-     * A listener when the current dimensions of the player change
-     */
-    lateinit var sizeChangedListener: () -> Unit
-    private var firstRefresh: Boolean = true
-    @VisibleForTesting
-    private var transitionLayout: TransitionLayout? = null
-    private val layoutController = TransitionLayoutController()
-    private var animationDelay: Long = 0
-    private var animationDuration: Long = 0
-    private var animateNextStateChange: Boolean = false
-    private val measurement = MeasurementOutput(0, 0)
-    private var type: TYPE = TYPE.PLAYER
-
-    /**
-     * A map containing all viewStates for all locations of this mediaState
-     */
-    private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
-
-    /**
-     * The ending location of the view where it ends when all animations and transitions have
-     * finished
-     */
-    @MediaLocation
-    var currentEndLocation: Int = -1
-
-    /**
-     * The starting location of the view where it starts for all animations and transitions
-     */
-    @MediaLocation
-    private var currentStartLocation: Int = -1
-
-    /**
-     * The progress of the transition or 1.0 if there is no transition happening
-     */
-    private var currentTransitionProgress: Float = 1.0f
-
-    /**
-     * A temporary state used to store intermediate measurements.
-     */
-    private val tmpState = TransitionViewState()
-
-    /**
-     * A temporary state used to store intermediate measurements.
-     */
-    private val tmpState2 = TransitionViewState()
-
-    /**
-     * A temporary state used to store intermediate measurements.
-     */
-    private val tmpState3 = TransitionViewState()
-
-    /**
-     * A temporary cache key to be used to look up cache entries
-     */
-    private val tmpKey = CacheKey()
-
-    /**
-     * The current width of the player. This might not factor in case the player is animating
-     * to the current state, but represents the end state
-     */
-    var currentWidth: Int = 0
-    /**
-     * The current height of the player. This might not factor in case the player is animating
-     * to the current state, but represents the end state
-     */
-    var currentHeight: Int = 0
-
-    /**
-     * Get the translationX of the layout
-     */
-    var translationX: Float = 0.0f
-        private set
-        get() {
-            return transitionLayout?.translationX ?: 0.0f
-        }
-
-    /**
-     * Get the translationY of the layout
-     */
-    var translationY: Float = 0.0f
-        private set
-        get() {
-            return transitionLayout?.translationY ?: 0.0f
-        }
-
-    /**
-     * A callback for RTL config changes
-     */
-    private val configurationListener = object : ConfigurationController.ConfigurationListener {
-        override fun onConfigChanged(newConfig: Configuration?) {
-            // Because the TransitionLayout is not always attached (and calculates/caches layout
-            // results regardless of attach state), we have to force the layoutDirection of the view
-            // to the correct value for the user's current locale to ensure correct recalculation
-            // when/after calling refreshState()
-            newConfig?.apply {
-                if (transitionLayout?.rawLayoutDirection != layoutDirection) {
-                    transitionLayout?.layoutDirection = layoutDirection
-                    refreshState()
-                }
-            }
-        }
-    }
-
-    /**
-     * A callback for media state changes
-     */
-    val stateCallback = object : MediaHostStatesManager.Callback {
-        override fun onHostStateChanged(
-            @MediaLocation location: Int,
-            mediaHostState: MediaHostState
-        ) {
-            if (location == currentEndLocation || location == currentStartLocation) {
-                setCurrentState(currentStartLocation,
-                        currentEndLocation,
-                        currentTransitionProgress,
-                        applyImmediately = false)
-            }
-        }
-    }
-
-    /**
-     * The expanded constraint set used to render a expanded player. If it is modified, make sure
-     * to call [refreshState]
-     */
-    val collapsedLayout = ConstraintSet()
-
-    /**
-     * The expanded constraint set used to render a collapsed player. If it is modified, make sure
-     * to call [refreshState]
-     */
-    val expandedLayout = ConstraintSet()
-
-    /**
-     * Whether the guts are visible for the associated player.
-     */
-    var isGutsVisible = false
-        private set
-
-    init {
-        mediaHostStatesManager.addController(this)
-        layoutController.sizeChangedListener = { width: Int, height: Int ->
-            currentWidth = width
-            currentHeight = height
-            sizeChangedListener.invoke()
-        }
-        configurationController.addCallback(configurationListener)
-    }
-
-    /**
-     * Notify this controller that the view has been removed and all listeners should be destroyed
-     */
-    fun onDestroy() {
-        mediaHostStatesManager.removeController(this)
-        configurationController.removeCallback(configurationListener)
-    }
-
-    /**
-     * Show guts with an animated transition.
-     */
-    fun openGuts() {
-        if (isGutsVisible) return
-        isGutsVisible = true
-        animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
-        setCurrentState(currentStartLocation,
-                currentEndLocation,
-                currentTransitionProgress,
-                applyImmediately = false)
-    }
-
-    /**
-     * Close the guts for the associated player.
-     *
-     * @param immediate if `false`, it will animate the transition.
-     */
-    @JvmOverloads
-    fun closeGuts(immediate: Boolean = false) {
-        if (!isGutsVisible) return
-        isGutsVisible = false
-        if (!immediate) {
-            animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
-        }
-        setCurrentState(currentStartLocation,
-                currentEndLocation,
-                currentTransitionProgress,
-                applyImmediately = immediate)
-    }
-
-    private fun ensureAllMeasurements() {
-        val mediaStates = mediaHostStatesManager.mediaHostStates
-        for (entry in mediaStates) {
-            obtainViewState(entry.value)
-        }
-    }
-
-    /**
-     * Get the constraintSet for a given expansion
-     */
-    private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
-            if (expansion > 0) expandedLayout else collapsedLayout
-
-    /**
-     * Set the views to be showing/hidden based on the [isGutsVisible] for a given
-     * [TransitionViewState].
-     */
-    private fun setGutsViewState(viewState: TransitionViewState) {
-        val controlsIds = when (type) {
-            TYPE.PLAYER -> MediaViewHolder.controlsIds
-            TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
-        }
-        val gutsIds = GutsViewHolder.ids
-        controlsIds.forEach { id ->
-            viewState.widgetStates.get(id)?.let { state ->
-                // Make sure to use the unmodified state if guts are not visible.
-                state.alpha = if (isGutsVisible) 0f else state.alpha
-                state.gone = if (isGutsVisible) true else state.gone
-            }
-        }
-        gutsIds.forEach { id ->
-            viewState.widgetStates.get(id)?.let { state ->
-                // Make sure to use the unmodified state if guts are visible
-                state.alpha = if (isGutsVisible) state.alpha else 0f
-                state.gone = if (isGutsVisible) state.gone else true
-            }
-        }
-    }
-
-    /**
-     * Apply squishFraction to a copy of viewState such that the cached version is untouched.
-    */
-    internal fun squishViewState(
-        viewState: TransitionViewState,
-        squishFraction: Float
-    ): TransitionViewState {
-        val squishedViewState = viewState.copy()
-        squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
-        controlIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
-            }
-        }
-
-        detailIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
-            }
-        }
-
-        RecommendationViewHolder.mediaContainersIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
-            }
-        }
-
-        RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
-            squishedViewState.widgetStates.get(id)?.let { state ->
-                state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
-            }
-        }
-
-        return squishedViewState
-    }
-
-    /**
-     * Obtain a new viewState for a given media state. This usually returns a cached state, but if
-     * it's not available, it will recreate one by measuring, which may be expensive.
-     */
-     @VisibleForTesting
-     fun obtainViewState(state: MediaHostState?): TransitionViewState? {
-        if (state == null || state.measurementInput == null) {
-            return null
-        }
-        // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
-        var cacheKey = getKey(state, isGutsVisible, tmpKey)
-        val viewState = viewStates[cacheKey]
-        if (viewState != null) {
-            // we already have cached this measurement, let's continue
-            if (state.squishFraction <= 1f) {
-                return squishViewState(viewState, state.squishFraction)
-            }
-            return viewState
-        }
-        // Copy the key since this might call recursively into it and we're using tmpKey
-        cacheKey = cacheKey.copy()
-        val result: TransitionViewState?
-
-        if (transitionLayout == null) {
-            return null
-        }
-        // Let's create a new measurement
-        if (state.expansion == 0.0f || state.expansion == 1.0f) {
-            result = transitionLayout!!.calculateViewState(
-                    state.measurementInput!!,
-                    constraintSetForExpansion(state.expansion),
-                    TransitionViewState())
-
-            setGutsViewState(result)
-            // We don't want to cache interpolated or null states as this could quickly fill up
-            // our cache. We only cache the start and the end states since the interpolation
-            // is cheap
-            viewStates[cacheKey] = result
-        } else {
-            // This is an interpolated state
-            val startState = state.copy().also { it.expansion = 0.0f }
-
-            // Given that we have a measurement and a view, let's get (guaranteed) viewstates
-            // from the start and end state and interpolate them
-            val startViewState = obtainViewState(startState) as TransitionViewState
-            val endState = state.copy().also { it.expansion = 1.0f }
-            val endViewState = obtainViewState(endState) as TransitionViewState
-            result = layoutController.getInterpolatedState(
-                    startViewState,
-                    endViewState,
-                    state.expansion)
-        }
-        if (state.squishFraction <= 1f) {
-            return squishViewState(result, state.squishFraction)
-        }
-        return result
-    }
-
-    private fun getKey(
-        state: MediaHostState,
-        guts: Boolean,
-        result: CacheKey
-    ): CacheKey {
-        result.apply {
-            heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
-            widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
-            expansion = state.expansion
-            gutsVisible = guts
-        }
-        return result
-    }
-
-    /**
-     * Attach a view to this controller. This may perform measurements if it's not available yet
-     * and should therefore be done carefully.
-     */
-    fun attach(
-        transitionLayout: TransitionLayout,
-        type: TYPE
-    ) = traceSection("MediaViewController#attach") {
-        updateMediaViewControllerType(type)
-        logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
-        this.transitionLayout = transitionLayout
-        layoutController.attach(transitionLayout)
-        if (currentEndLocation == -1) {
-            return
-        }
-        // Set the previously set state immediately to the view, now that it's finally attached
-        setCurrentState(
-                startLocation = currentStartLocation,
-                endLocation = currentEndLocation,
-                transitionProgress = currentTransitionProgress,
-                applyImmediately = true)
-    }
-
-    /**
-     * Obtain a measurement for a given location. This makes sure that the state is up to date
-     * and all widgets know their location. Calling this method may create a measurement if we
-     * don't have a cached value available already.
-     */
-    fun getMeasurementsForState(
-        hostState: MediaHostState
-    ): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") {
-        val viewState = obtainViewState(hostState) ?: return null
-        measurement.measuredWidth = viewState.width
-        measurement.measuredHeight = viewState.height
-        return measurement
-    }
-
-    /**
-     * Set a new state for the controlled view which can be an interpolation between multiple
-     * locations.
-     */
-    fun setCurrentState(
-        @MediaLocation startLocation: Int,
-        @MediaLocation endLocation: Int,
-        transitionProgress: Float,
-        applyImmediately: Boolean
-    ) = traceSection("MediaViewController#setCurrentState") {
-        currentEndLocation = endLocation
-        currentStartLocation = startLocation
-        currentTransitionProgress = transitionProgress
-        logger.logMediaLocation("setCurrentState", startLocation, endLocation)
-
-        val shouldAnimate = animateNextStateChange && !applyImmediately
-
-        val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
-        val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
-
-        // Obtain the view state that we'd want to be at the end
-        // The view might not be bound yet or has never been measured and in that case will be
-        // reset once the state is fully available
-        var endViewState = obtainViewState(endHostState) ?: return
-        endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
-        layoutController.setMeasureState(endViewState)
-
-        // If the view isn't bound, we can drop the animation, otherwise we'll execute it
-        animateNextStateChange = false
-        if (transitionLayout == null) {
-            return
-        }
-
-        val result: TransitionViewState
-        var startViewState = obtainViewState(startHostState)
-        startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
-
-        if (!endHostState.visible) {
-            // Let's handle the case where the end is gone first. In this case we take the
-            // start viewState and will make it gone
-            if (startViewState == null || startHostState == null || !startHostState.visible) {
-                // the start isn't a valid state, let's use the endstate directly
-                result = endViewState
-            } else {
-                // Let's get the gone presentation from the start state
-                result = layoutController.getGoneState(startViewState,
-                        startHostState.disappearParameters,
-                        transitionProgress,
-                        tmpState)
-            }
-        } else if (startHostState != null && !startHostState.visible) {
-            // We have a start state and it is gone.
-            // Let's get presentation from the endState
-            result = layoutController.getGoneState(endViewState, endHostState.disappearParameters,
-                    1.0f - transitionProgress,
-                    tmpState)
-        } else if (transitionProgress == 1.0f || startViewState == null) {
-            // We're at the end. Let's use that state
-            result = endViewState
-        } else if (transitionProgress == 0.0f) {
-            // We're at the start. Let's use that state
-            result = startViewState
-        } else {
-            result = layoutController.getInterpolatedState(startViewState, endViewState,
-                    transitionProgress, tmpState)
-        }
-        logger.logMediaSize("setCurrentState", result.width, result.height)
-        layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
-                animationDelay)
-    }
-
-    private fun updateViewStateToCarouselSize(
-        viewState: TransitionViewState?,
-        location: Int,
-        outState: TransitionViewState
-    ): TransitionViewState? {
-        val result = viewState?.copy(outState) ?: return null
-        val overrideSize = mediaHostStatesManager.carouselSizes[location]
-        overrideSize?.let {
-            // To be safe we're using a maximum here. The override size should always be set
-            // properly though.
-            result.height = Math.max(it.measuredHeight, result.height)
-            result.width = Math.max(it.measuredWidth, result.width)
-        }
-        logger.logMediaSize("update to carousel", result.width, result.height)
-        return result
-    }
-
-    private fun updateMediaViewControllerType(type: TYPE) {
-        this.type = type
-
-        // These XML resources contain ConstraintSets that will apply to this player type's layout
-        when (type) {
-            TYPE.PLAYER -> {
-                collapsedLayout.load(context, R.xml.media_session_collapsed)
-                expandedLayout.load(context, R.xml.media_session_expanded)
-            }
-            TYPE.RECOMMENDATION -> {
-                collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
-                expandedLayout.load(context, R.xml.media_recommendation_expanded)
-            }
-        }
-        refreshState()
-    }
-
-    /**
-     * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation].
-     * In the event of [location] not being visible, [locationWhenHidden] will be used instead.
-     *
-     * @param location Target
-     * @param locationWhenHidden Location that will be used when the target is not
-     * [MediaHost.visible]
-     * @return State require for executing a transition, and also the respective [MediaHost].
-     */
-    private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
-        val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
-        return obtainViewState(mediaHostState)
-    }
-
-    /**
-     * Notify that the location is changing right now and a [setCurrentState] change is imminent.
-     * This updates the width the view will me measured with.
-     */
-    fun onLocationPreChange(@MediaLocation newLocation: Int) {
-        obtainViewStateForLocation(newLocation)?.let {
-            layoutController.setMeasureState(it)
-        }
-    }
-
-    /**
-     * Request that the next state change should be animated with the given parameters.
-     */
-    fun animatePendingStateChange(duration: Long, delay: Long) {
-        animateNextStateChange = true
-        animationDuration = duration
-        animationDelay = delay
-    }
-
-    /**
-     * Clear all existing measurements and refresh the state to match the view.
-     */
-    fun refreshState() = traceSection("MediaViewController#refreshState") {
-        // Let's clear all of our measurements and recreate them!
-        viewStates.clear()
-        if (firstRefresh) {
-            // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
-            // We'll just load these on demand.
-            ensureAllMeasurements()
-            firstRefresh = false
-        }
-        setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
-                applyImmediately = true)
-    }
-}
-
-/**
- * An internal key for the cache of mediaViewStates. This is a subset of the full host state.
- */
-private data class CacheKey(
-    var widthMeasureSpec: Int = -1,
-    var heightMeasureSpec: Int = -1,
-    var expansion: Float = 0.0f,
-    var gutsVisible: Boolean = false
-)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
deleted file mode 100644
index 51c658c..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewLogger.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.dagger.MediaViewLog
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import javax.inject.Inject
-
-private const val TAG = "MediaView"
-
-/**
- * A buffered log for media view events that are too noisy for regular logging
- */
-@SysUISingleton
-class MediaViewLogger @Inject constructor(
-    @MediaViewLog private val buffer: LogBuffer
-) {
-    fun logMediaSize(reason: String, width: Int, height: Int) {
-        buffer.log(
-                TAG,
-                LogLevel.DEBUG,
-                {
-                    str1 = reason
-                    int1 = width
-                    int2 = height
-                },
-                {
-                    "size ($str1): $int1 x $int2"
-                }
-        )
-    }
-
-    fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
-        buffer.log(
-                TAG,
-                LogLevel.DEBUG,
-                {
-                    str1 = reason
-                    int1 = startLocation
-                    int2 = endLocation
-                },
-                {
-                    "location ($str1): $int1 -> $int2"
-                }
-        )
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
deleted file mode 100644
index 8ae75fc..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/RecommendationViewHolder.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import com.android.systemui.R
-import com.android.systemui.util.animation.TransitionLayout
-
-private const val TAG = "RecommendationViewHolder"
-
-/** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View) {
-
-    val recommendations = itemView as TransitionLayout
-
-    // Recommendation screen
-    val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
-    val mediaCoverItems = listOf<ImageView>(
-        itemView.requireViewById(R.id.media_cover1),
-        itemView.requireViewById(R.id.media_cover2),
-        itemView.requireViewById(R.id.media_cover3)
-    )
-    val mediaCoverContainers = listOf<ViewGroup>(
-        itemView.requireViewById(R.id.media_cover1_container),
-        itemView.requireViewById(R.id.media_cover2_container),
-        itemView.requireViewById(R.id.media_cover3_container)
-    )
-    val mediaTitles: List<TextView> = listOf(
-        itemView.requireViewById(R.id.media_title1),
-        itemView.requireViewById(R.id.media_title2),
-        itemView.requireViewById(R.id.media_title3)
-    )
-    val mediaSubtitles: List<TextView> = listOf(
-        itemView.requireViewById(R.id.media_subtitle1),
-        itemView.requireViewById(R.id.media_subtitle2),
-        itemView.requireViewById(R.id.media_subtitle3)
-    )
-
-    val gutsViewHolder = GutsViewHolder(itemView)
-
-    init {
-        (recommendations.background as IlluminationDrawable).let { background ->
-            mediaCoverContainers.forEach { background.registerLightSource(it) }
-            background.registerLightSource(gutsViewHolder.cancel)
-            background.registerLightSource(gutsViewHolder.dismiss)
-            background.registerLightSource(gutsViewHolder.settings)
-        }
-    }
-
-    fun marquee(start: Boolean, delay: Long) {
-        gutsViewHolder.marquee(start, delay, TAG)
-    }
-
-    companion object {
-        /**
-         * Creates a RecommendationViewHolder.
-         *
-         * @param inflater LayoutInflater to use to inflate the layout.
-         * @param parent Parent of inflated view.
-         */
-        @JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup):
-            RecommendationViewHolder {
-            val itemView =
-                inflater.inflate(
-                    R.layout.media_smartspace_recommendations,
-                    parent,
-                    false /* attachToRoot */)
-            // Because this media view (a TransitionLayout) is used to measure and layout the views
-            // in various states before being attached to its parent, we can't depend on the default
-            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
-            itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
-            return RecommendationViewHolder(itemView)
-        }
-
-        // Res Ids for the control components on the recommendation view.
-        val controlsIds = setOf(
-            R.id.recommendation_card_icon,
-            R.id.media_cover1,
-            R.id.media_cover2,
-            R.id.media_cover3,
-            R.id.media_cover1_container,
-            R.id.media_cover2_container,
-            R.id.media_cover3_container,
-            R.id.media_title1,
-            R.id.media_title2,
-            R.id.media_title3,
-            R.id.media_subtitle1,
-            R.id.media_subtitle2,
-            R.id.media_subtitle3
-        )
-
-        val mediaTitlesAndSubtitlesIds = setOf(
-            R.id.media_title1,
-            R.id.media_title2,
-            R.id.media_title3,
-            R.id.media_subtitle1,
-            R.id.media_subtitle2,
-            R.id.media_subtitle3
-        )
-
-        val mediaContainersIds = setOf(
-            R.id.media_cover1_container,
-            R.id.media_cover2_container,
-            R.id.media_cover3_container
-        )
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
deleted file mode 100644
index a9c5c61..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserLogger.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.content.ComponentName
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.dagger.MediaBrowserLog
-import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.plugins.log.LogLevel
-import javax.inject.Inject
-
-/** A logger for events in [ResumeMediaBrowser]. */
-@SysUISingleton
-class ResumeMediaBrowserLogger @Inject constructor(
-    @MediaBrowserLog private val buffer: LogBuffer
-) {
-    /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */
-    fun logConnection(componentName: ComponentName, reason: String) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = componentName.toShortString()
-            str2 = reason
-        },
-        { "Connecting browser for component $str1 due to $str2" }
-    )
-
-    /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */
-    fun logDisconnect(componentName: ComponentName) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            str1 = componentName.toShortString()
-        },
-        { "Disconnecting browser for component $str1" }
-    )
-
-    /**
-     * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed]
-     * event.
-     *
-     * @param isBrowserConnected true if there's a currently connected
-     *     [android.media.browse.MediaBrowser] and false otherwise.
-     * @param componentName the component name for the [ResumeMediaBrowser] that triggered this log.
-     */
-    fun logSessionDestroyed(
-        isBrowserConnected: Boolean,
-        componentName: ComponentName
-    ) = buffer.log(
-        TAG,
-        LogLevel.DEBUG,
-        {
-            bool1 = isBrowserConnected
-            str1 = componentName.toShortString()
-        },
-        { "Session destroyed. Active browser = $bool1. Browser component = $str1." }
-    )
-}
-
-private const val TAG = "MediaBrowser"
diff --git a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
index 73240b5..5315067 100644
--- a/packages/SystemUI/src/com/android/systemui/media/GutsViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/GutsViewHolder.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models
 
 import android.content.res.ColorStateList
 import android.util.Log
@@ -23,6 +23,9 @@
 import android.widget.ImageButton
 import android.widget.TextView
 import com.android.systemui.R
+import com.android.systemui.media.controls.ui.accentPrimaryFromScheme
+import com.android.systemui.media.controls.ui.surfaceFromScheme
+import com.android.systemui.media.controls.ui.textPrimaryFromScheme
 import com.android.systemui.monet.ColorScheme
 
 /**
@@ -95,11 +98,6 @@
     }
 
     companion object {
-        val ids = setOf(
-            R.id.remove_text,
-            R.id.cancel,
-            R.id.dismiss,
-            R.id.settings
-        )
+        val ids = setOf(R.id.remove_text, R.id.cancel, R.id.dismiss, R.id.settings)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
similarity index 66%
rename from packages/SystemUI/src/com/android/systemui/media/MediaData.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
index d0fc3d06..ed649b1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.player
 
 import android.app.PendingIntent
 import android.graphics.drawable.Drawable
@@ -27,69 +27,42 @@
 data class MediaData(
     val userId: Int,
     val initialized: Boolean = false,
-    /**
-     * App name that will be displayed on the player.
-     */
+    /** App name that will be displayed on the player. */
     val app: String?,
-    /**
-     * App icon shown on player.
-     */
+    /** App icon shown on player. */
     val appIcon: Icon?,
-    /**
-     * Artist name.
-     */
+    /** Artist name. */
     val artist: CharSequence?,
-    /**
-     * Song name.
-     */
+    /** Song name. */
     val song: CharSequence?,
-    /**
-     * Album artwork.
-     */
+    /** Album artwork. */
     val artwork: Icon?,
-    /**
-     * List of generic action buttons for the media player, based on notification actions
-     */
+    /** List of generic action buttons for the media player, based on notification actions */
     val actions: List<MediaAction>,
-    /**
-     * Same as above, but shown on smaller versions of the player, like in QQS or keyguard.
-     */
+    /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
     val actionsToShowInCompact: List<Int>,
     /**
-     * Semantic actions buttons, based on the PlaybackState of the media session.
-     * If present, these actions will be preferred in the UI over [actions]
+     * Semantic actions buttons, based on the PlaybackState of the media session. If present, these
+     * actions will be preferred in the UI over [actions]
      */
     val semanticActions: MediaButton? = null,
-    /**
-     * Package name of the app that's posting the media.
-     */
+    /** Package name of the app that's posting the media. */
     val packageName: String,
-    /**
-     * Unique media session identifier.
-     */
+    /** Unique media session identifier. */
     val token: MediaSession.Token?,
-    /**
-     * Action to perform when the player is tapped.
-     * This is unrelated to {@link #actions}.
-     */
+    /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */
     val clickIntent: PendingIntent?,
-    /**
-     * Where the media is playing: phone, headphones, ear buds, remote session.
-     */
+    /** Where the media is playing: phone, headphones, ear buds, remote session. */
     val device: MediaDeviceData?,
     /**
-     * When active, a player will be displayed on keyguard and quick-quick settings.
-     * This is unrelated to the stream being playing or not, a player will not be active if
-     * timed out, or in resumption mode.
+     * When active, a player will be displayed on keyguard and quick-quick settings. This is
+     * unrelated to the stream being playing or not, a player will not be active if timed out, or in
+     * resumption mode.
      */
     var active: Boolean,
-    /**
-     * Action that should be performed to restart a non active session.
-     */
+    /** Action that should be performed to restart a non active session. */
     var resumeAction: Runnable?,
-    /**
-     * Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE
-     */
+    /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */
     var playbackLocation: Int = PLAYBACK_LOCAL,
     /**
      * Indicates that this player is a resumption player (ie. It only shows a play actions which
@@ -102,29 +75,19 @@
     val notificationKey: String? = null,
     var hasCheckedForResume: Boolean = false,
 
-    /**
-     * If apps do not report PlaybackState, set as null to imply 'undetermined'
-     */
+    /** If apps do not report PlaybackState, set as null to imply 'undetermined' */
     val isPlaying: Boolean? = null,
 
-    /**
-     * Set from the notification and used as fallback when PlaybackState cannot be determined
-     */
+    /** Set from the notification and used as fallback when PlaybackState cannot be determined */
     val isClearable: Boolean = true,
 
-    /**
-     * Timestamp when this player was last active.
-     */
+    /** Timestamp when this player was last active. */
     var lastActive: Long = 0L,
 
-    /**
-     * Instance ID for logging purposes
-     */
+    /** Instance ID for logging purposes */
     val instanceId: InstanceId,
 
-    /**
-     * The UID of the app, used for logging
-     */
+    /** The UID of the app, used for logging */
     val appUid: Int
 ) {
     companion object {
@@ -141,37 +104,21 @@
     }
 }
 
-/**
- * Contains [MediaAction] objects which represent specific buttons in the UI
- */
+/** Contains [MediaAction] objects which represent specific buttons in the UI */
 data class MediaButton(
-    /**
-     * Play/pause button
-     */
+    /** Play/pause button */
     val playOrPause: MediaAction? = null,
-    /**
-     * Next button, or custom action
-     */
+    /** Next button, or custom action */
     val nextOrCustom: MediaAction? = null,
-    /**
-     * Previous button, or custom action
-     */
+    /** Previous button, or custom action */
     val prevOrCustom: MediaAction? = null,
-    /**
-     * First custom action space
-     */
+    /** First custom action space */
     val custom0: MediaAction? = null,
-    /**
-     * Second custom action space
-     */
+    /** Second custom action space */
     val custom1: MediaAction? = null,
-    /**
-     * Whether to reserve the empty space when the nextOrCustom is null
-     */
+    /** Whether to reserve the empty space when the nextOrCustom is null */
     val reserveNext: Boolean = false,
-    /**
-     * Whether to reserve the empty space when the prevOrCustom is null
-     */
+    /** Whether to reserve the empty space when the prevOrCustom is null */
     val reservePrev: Boolean = false
 ) {
     fun getActionById(id: Int): MediaAction? {
@@ -201,7 +148,8 @@
 
 /** State of the media device. */
 data class MediaDeviceData
-@JvmOverloads constructor(
+@JvmOverloads
+constructor(
     /** Whether or not to enable the chip */
     val enabled: Boolean,
 
@@ -221,8 +169,8 @@
     val showBroadcastButton: Boolean
 ) {
     /**
-     * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon
-     * is ignored because it can change by reference frequently depending on the device type's
+     * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is
+     * ignored because it can change by reference frequently depending on the device type's
      * implementation, but this is not usually relevant unless other info has changed
      */
     fun equalsWithoutIcon(other: MediaDeviceData?): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
index fc9515c..2511324 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.player
 
 import android.view.LayoutInflater
 import android.view.View
@@ -25,13 +25,12 @@
 import android.widget.TextView
 import androidx.constraintlayout.widget.Barrier
 import com.android.systemui.R
+import com.android.systemui.media.controls.models.GutsViewHolder
 import com.android.systemui.util.animation.TransitionLayout
 
 private const val TAG = "MediaViewHolder"
 
-/**
- * Holder class for media player view
- */
+/** Holder class for media player view */
 class MediaViewHolder constructor(itemView: View) {
     val player = itemView as TransitionLayout
 
@@ -52,8 +51,7 @@
     // These views are only shown while the user is actively scrubbing
     val scrubbingElapsedTimeView: TextView =
         itemView.requireViewById(R.id.media_scrubbing_elapsed_time)
-    val scrubbingTotalTimeView: TextView =
-        itemView.requireViewById(R.id.media_scrubbing_total_time)
+    val scrubbingTotalTimeView: TextView = itemView.requireViewById(R.id.media_scrubbing_total_time)
 
     val gutsViewHolder = GutsViewHolder(itemView)
 
@@ -86,15 +84,7 @@
     }
 
     fun getTransparentActionButtons(): List<ImageButton> {
-        return listOf(
-                actionNext,
-                actionPrev,
-                action0,
-                action1,
-                action2,
-                action3,
-                action4
-        )
+        return listOf(actionNext, actionPrev, action0, action1, action2, action3, action4)
     }
 
     fun marquee(start: Boolean, delay: Long) {
@@ -108,10 +98,8 @@
          * @param inflater LayoutInflater to use to inflate the layout.
          * @param parent Parent of inflated view.
          */
-        @JvmStatic fun create(
-            inflater: LayoutInflater,
-            parent: ViewGroup
-        ): MediaViewHolder {
+        @JvmStatic
+        fun create(inflater: LayoutInflater, parent: ViewGroup): MediaViewHolder {
             val mediaView = inflater.inflate(R.layout.media_session_view, parent, false)
             mediaView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
             // Because this media view (a TransitionLayout) is used to measure and layout the views
@@ -124,7 +112,8 @@
             }
         }
 
-        val controlsIds = setOf(
+        val controlsIds =
+            setOf(
                 R.id.icon,
                 R.id.app_name,
                 R.id.header_title,
@@ -142,27 +131,23 @@
                 R.id.icon,
                 R.id.media_scrubbing_elapsed_time,
                 R.id.media_scrubbing_total_time
-        )
+            )
 
         // Buttons used for notification-based actions
-        val genericButtonIds = setOf(
-            R.id.action0,
-            R.id.action1,
-            R.id.action2,
-            R.id.action3,
-            R.id.action4
-        )
+        val genericButtonIds =
+            setOf(R.id.action0, R.id.action1, R.id.action2, R.id.action3, R.id.action4)
 
-        val expandedBottomActionIds = setOf(
-            R.id.actionPrev,
-            R.id.actionNext,
-            R.id.action0,
-            R.id.action1,
-            R.id.action2,
-            R.id.action3,
-            R.id.action4,
-            R.id.media_scrubbing_elapsed_time,
-            R.id.media_scrubbing_total_time
-        )
+        val expandedBottomActionIds =
+            setOf(
+                R.id.actionPrev,
+                R.id.actionNext,
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
index 121021f..37d956b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarObserver.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.player
 
 import android.animation.Animator
 import android.animation.ObjectAnimator
@@ -24,40 +24,56 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.media.controls.ui.SquigglyProgress
 
 /**
  * Observer for changes from SeekBarViewModel.
  *
  * <p>Updates the seek bar views in response to changes to the model.
  */
-open class SeekBarObserver(
-    private val holder: MediaViewHolder
-) : Observer<SeekBarViewModel.Progress> {
+open class SeekBarObserver(private val holder: MediaViewHolder) :
+    Observer<SeekBarViewModel.Progress> {
 
     companion object {
         @JvmStatic val RESET_ANIMATION_DURATION_MS: Int = 750
         @JvmStatic val RESET_ANIMATION_THRESHOLD_MS: Int = 250
     }
 
-    val seekBarEnabledMaxHeight = holder.seekBar.context.resources
-        .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height)
-    val seekBarDisabledHeight = holder.seekBar.context.resources
-        .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height)
-    val seekBarEnabledVerticalPadding = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_session_enabled_seekbar_vertical_padding)
-    val seekBarDisabledVerticalPadding = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_session_disabled_seekbar_vertical_padding)
+    val seekBarEnabledMaxHeight =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_enabled_seekbar_height
+        )
+    val seekBarDisabledHeight =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_disabled_seekbar_height
+        )
+    val seekBarEnabledVerticalPadding =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_session_enabled_seekbar_vertical_padding
+        )
+    val seekBarDisabledVerticalPadding =
+        holder.seekBar.context.resources.getDimensionPixelSize(
+            R.dimen.qs_media_session_disabled_seekbar_vertical_padding
+        )
     var seekBarResetAnimator: Animator? = null
 
     init {
-        val seekBarProgressWavelength = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength).toFloat()
-        val seekBarProgressAmplitude = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude).toFloat()
-        val seekBarProgressPhase = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase).toFloat()
-        val seekBarProgressStrokeWidth = holder.seekBar.context.resources
-                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width).toFloat()
+        val seekBarProgressWavelength =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_wavelength)
+                .toFloat()
+        val seekBarProgressAmplitude =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_amplitude)
+                .toFloat()
+        val seekBarProgressPhase =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_phase)
+                .toFloat()
+        val seekBarProgressStrokeWidth =
+            holder.seekBar.context.resources
+                .getDimensionPixelSize(R.dimen.qs_media_seekbar_progress_stroke_width)
+                .toFloat()
         val progressDrawable = holder.seekBar.progressDrawable as? SquigglyProgress
         progressDrawable?.let {
             it.waveLength = seekBarProgressWavelength
@@ -97,16 +113,18 @@
         }
 
         holder.seekBar.setMax(data.duration)
-        val totalTimeString = DateUtils.formatElapsedTime(
-            data.duration / DateUtils.SECOND_IN_MILLIS)
+        val totalTimeString =
+            DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
         if (data.scrubbing) {
             holder.scrubbingTotalTimeView.text = totalTimeString
         }
 
         data.elapsedTime?.let {
             if (!data.scrubbing && !(seekBarResetAnimator?.isRunning ?: false)) {
-                if (it <= RESET_ANIMATION_THRESHOLD_MS &&
-                        holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS) {
+                if (
+                    it <= RESET_ANIMATION_THRESHOLD_MS &&
+                        holder.seekBar.progress > RESET_ANIMATION_THRESHOLD_MS
+                ) {
                     // This animation resets for every additional update to zero.
                     val animator = buildResetAnimator(it)
                     animator.start()
@@ -116,24 +134,29 @@
                 }
             }
 
-            val elapsedTimeString = DateUtils.formatElapsedTime(
-                it / DateUtils.SECOND_IN_MILLIS)
+            val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
             if (data.scrubbing) {
                 holder.scrubbingElapsedTimeView.text = elapsedTimeString
             }
 
-            holder.seekBar.contentDescription = holder.seekBar.context.getString(
-                R.string.controls_media_seekbar_description,
-                elapsedTimeString,
-                totalTimeString
-            )
+            holder.seekBar.contentDescription =
+                holder.seekBar.context.getString(
+                    R.string.controls_media_seekbar_description,
+                    elapsedTimeString,
+                    totalTimeString
+                )
         }
     }
 
     @VisibleForTesting
     open fun buildResetAnimator(targetTime: Int): Animator {
-        val animator = ObjectAnimator.ofInt(holder.seekBar, "progress",
-                holder.seekBar.progress, targetTime + RESET_ANIMATION_DURATION_MS)
+        val animator =
+            ObjectAnimator.ofInt(
+                holder.seekBar,
+                "progress",
+                holder.seekBar.progress,
+                targetTime + RESET_ANIMATION_DURATION_MS
+            )
         animator.setAutoCancel(true)
         animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
         animator.interpolator = Interpolators.EMPHASIZED
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
similarity index 74%
rename from packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index 0f78a1e..bba5f35 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.player
 
 import android.media.MediaMetadata
 import android.media.session.MediaController
@@ -42,8 +42,8 @@
 
 private fun PlaybackState.isInMotion(): Boolean {
     return this.state == PlaybackState.STATE_PLAYING ||
-            this.state == PlaybackState.STATE_FAST_FORWARDING ||
-            this.state == PlaybackState.STATE_REWINDING
+        this.state == PlaybackState.STATE_FAST_FORWARDING ||
+        this.state == PlaybackState.STATE_REWINDING
 }
 
 /**
@@ -59,8 +59,8 @@
         val updateTime = this.getLastPositionUpdateTime()
         val currentTime = SystemClock.elapsedRealtime()
         if (updateTime > 0) {
-            var position = (this.playbackSpeed * (currentTime - updateTime)).toLong() +
-                    this.getPosition()
+            var position =
+                (this.playbackSpeed * (currentTime - updateTime)).toLong() + this.getPosition()
             if (duration >= 0 && position > duration) {
                 position = duration.toLong()
             } else if (position < 0) {
@@ -73,7 +73,9 @@
 }
 
 /** ViewModel for seek bar in QS media player. */
-class SeekBarViewModel @Inject constructor(
+class SeekBarViewModel
+@Inject
+constructor(
     @Background private val bgExecutor: RepeatableExecutor,
     private val falsingManager: FalsingManager,
 ) {
@@ -86,9 +88,7 @@
             }
             _progress.postValue(value)
         }
-    private val _progress = MutableLiveData<Progress>().apply {
-        postValue(_data)
-    }
+    private val _progress = MutableLiveData<Progress>().apply { postValue(_data) }
     val progress: LiveData<Progress>
         get() = _progress
     private var controller: MediaController? = null
@@ -100,20 +100,21 @@
             }
         }
     private var playbackState: PlaybackState? = null
-    private var callback = object : MediaController.Callback() {
-        override fun onPlaybackStateChanged(state: PlaybackState?) {
-            playbackState = state
-            if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) {
+    private var callback =
+        object : MediaController.Callback() {
+            override fun onPlaybackStateChanged(state: PlaybackState?) {
+                playbackState = state
+                if (playbackState == null || PlaybackState.STATE_NONE.equals(playbackState)) {
+                    clearController()
+                } else {
+                    checkIfPollingNeeded()
+                }
+            }
+
+            override fun onSessionDestroyed() {
                 clearController()
-            } else {
-                checkIfPollingNeeded()
             }
         }
-
-        override fun onSessionDestroyed() {
-            clearController()
-        }
-    }
     private var cancel: Runnable? = null
 
     /** Indicates if the seek interaction is considered a false guesture. */
@@ -121,12 +122,13 @@
 
     /** Listening state (QS open or closed) is used to control polling of progress. */
     var listening = true
-        set(value) = bgExecutor.execute {
-            if (field != value) {
-                field = value
-                checkIfPollingNeeded()
+        set(value) =
+            bgExecutor.execute {
+                if (field != value) {
+                    field = value
+                    checkIfPollingNeeded()
+                }
             }
-        }
 
     private var scrubbingChangeListener: ScrubbingChangeListener? = null
     private var enabledChangeListener: EnabledChangeListener? = null
@@ -144,14 +146,13 @@
 
     lateinit var logSeek: () -> Unit
 
-    /**
-     * Event indicating that the user has started interacting with the seek bar.
-     */
+    /** Event indicating that the user has started interacting with the seek bar. */
     @AnyThread
-    fun onSeekStarting() = bgExecutor.execute {
-        scrubbing = true
-        isFalseSeek = false
-    }
+    fun onSeekStarting() =
+        bgExecutor.execute {
+            scrubbing = true
+            isFalseSeek = false
+        }
 
     /**
      * Event indicating that the user has moved the seek bar.
@@ -159,47 +160,51 @@
      * @param position Current location in the track.
      */
     @AnyThread
-    fun onSeekProgress(position: Long) = bgExecutor.execute {
-        if (scrubbing) {
-            // The user hasn't yet finished their touch gesture, so only update the data for visual
-            // feedback and don't update [controller] yet.
-            _data = _data.copy(elapsedTime = position.toInt())
-        } else {
-            // The seek progress came from an a11y action and we should immediately update to the
-            // new position. (a11y actions to change the seekbar position don't trigger
-            // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.)
-            onSeek(position)
+    fun onSeekProgress(position: Long) =
+        bgExecutor.execute {
+            if (scrubbing) {
+                // The user hasn't yet finished their touch gesture, so only update the data for
+                // visual
+                // feedback and don't update [controller] yet.
+                _data = _data.copy(elapsedTime = position.toInt())
+            } else {
+                // The seek progress came from an a11y action and we should immediately update to
+                // the
+                // new position. (a11y actions to change the seekbar position don't trigger
+                // SeekBar.OnSeekBarChangeListener.onStartTrackingTouch or onStopTrackingTouch.)
+                onSeek(position)
+            }
         }
-    }
 
-    /**
-     * Event indicating that the seek interaction is a false gesture and it should be ignored.
-     */
+    /** Event indicating that the seek interaction is a false gesture and it should be ignored. */
     @AnyThread
-    fun onSeekFalse() = bgExecutor.execute {
-        if (scrubbing) {
-            isFalseSeek = true
+    fun onSeekFalse() =
+        bgExecutor.execute {
+            if (scrubbing) {
+                isFalseSeek = true
+            }
         }
-    }
 
     /**
      * Handle request to change the current position in the media track.
      * @param position Place to seek to in the track.
      */
     @AnyThread
-    fun onSeek(position: Long) = bgExecutor.execute {
-        if (isFalseSeek) {
-            scrubbing = false
-            checkPlaybackPosition()
-        } else {
-            logSeek()
-            controller?.transportControls?.seekTo(position)
-            // Invalidate the cached playbackState to avoid the thumb jumping back to the previous
-            // position.
-            playbackState = null
-            scrubbing = false
+    fun onSeek(position: Long) =
+        bgExecutor.execute {
+            if (isFalseSeek) {
+                scrubbing = false
+                checkPlaybackPosition()
+            } else {
+                logSeek()
+                controller?.transportControls?.seekTo(position)
+                // Invalidate the cached playbackState to avoid the thumb jumping back to the
+                // previous
+                // position.
+                playbackState = null
+                scrubbing = false
+            }
         }
-    }
 
     /**
      * Updates media information.
@@ -216,11 +221,18 @@
         val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
         val position = playbackState?.position?.toInt()
         val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0
-        val playing = NotificationMediaManager
-                .isPlayingState(playbackState?.state ?: PlaybackState.STATE_NONE)
-        val enabled = if (playbackState == null ||
-                playbackState?.getState() == PlaybackState.STATE_NONE ||
-                (duration <= 0)) false else true
+        val playing =
+            NotificationMediaManager.isPlayingState(
+                playbackState?.state ?: PlaybackState.STATE_NONE
+            )
+        val enabled =
+            if (
+                playbackState == null ||
+                    playbackState?.getState() == PlaybackState.STATE_NONE ||
+                    (duration <= 0)
+            )
+                false
+            else true
         _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration)
         checkIfPollingNeeded()
     }
@@ -231,26 +243,26 @@
      * This should be called when the media session behind the controller has been destroyed.
      */
     @AnyThread
-    fun clearController() = bgExecutor.execute {
-        controller = null
-        playbackState = null
-        cancel?.run()
-        cancel = null
-        _data = _data.copy(enabled = false)
-    }
+    fun clearController() =
+        bgExecutor.execute {
+            controller = null
+            playbackState = null
+            cancel?.run()
+            cancel = null
+            _data = _data.copy(enabled = false)
+        }
 
-    /**
-     * Call to clean up any resources.
-     */
+    /** Call to clean up any resources. */
     @AnyThread
-    fun onDestroy() = bgExecutor.execute {
-        controller = null
-        playbackState = null
-        cancel?.run()
-        cancel = null
-        scrubbingChangeListener = null
-        enabledChangeListener = null
-    }
+    fun onDestroy() =
+        bgExecutor.execute {
+            controller = null
+            playbackState = null
+            cancel?.run()
+            cancel = null
+            scrubbingChangeListener = null
+            enabledChangeListener = null
+        }
 
     @WorkerThread
     private fun checkPlaybackPosition() {
@@ -266,8 +278,12 @@
         val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false
         if (needed) {
             if (cancel == null) {
-                cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L,
-                        POSITION_UPDATE_INTERVAL_MILLIS)
+                cancel =
+                    bgExecutor.executeRepeatedly(
+                        this::checkPlaybackPosition,
+                        0L,
+                        POSITION_UPDATE_INTERVAL_MILLIS
+                    )
             }
         } else {
             cancel?.run()
@@ -353,9 +369,10 @@
         // Gesture detector helps decide which touch events to intercept.
         private val detector = GestureDetectorCompat(bar.context, this)
         // Velocity threshold used to decide when a fling is considered a false gesture.
-        private val flingVelocity: Int = ViewConfiguration.get(bar.context).run {
-            getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR
-        }
+        private val flingVelocity: Int =
+            ViewConfiguration.get(bar.context).run {
+                getScaledMinimumFlingVelocity() * MIN_FLING_VELOCITY_SCALE_FACTOR
+            }
         // Indicates if the gesture should go to the seek bar or if it should be intercepted.
         private var shouldGoToSeekBar = false
 
@@ -385,9 +402,9 @@
         /**
          * Handle down events that press down on the thumb.
          *
-         * On the down action, determine a target box around the thumb to know when a scroll
-         * gesture starts by clicking on the thumb. The target box will be used by subsequent
-         * onScroll events.
+         * On the down action, determine a target box around the thumb to know when a scroll gesture
+         * starts by clicking on the thumb. The target box will be used by subsequent onScroll
+         * events.
          *
          * Returns true when the down event hits within the target box of the thumb.
          */
@@ -398,17 +415,19 @@
             // TODO: account for thumb offset
             val progress = bar.getProgress()
             val range = bar.max - bar.min
-            val widthFraction = if (range > 0) {
-                (progress - bar.min).toDouble() / range
-            } else {
-                0.0
-            }
+            val widthFraction =
+                if (range > 0) {
+                    (progress - bar.min).toDouble() / range
+                } else {
+                    0.0
+                }
             val availableWidth = bar.width - padL - padR
-            val thumbX = if (bar.isLayoutRtl()) {
-                padL + availableWidth * (1 - widthFraction)
-            } else {
-                padL + availableWidth * widthFraction
-            }
+            val thumbX =
+                if (bar.isLayoutRtl()) {
+                    padL + availableWidth * (1 - widthFraction)
+                } else {
+                    padL + availableWidth * widthFraction
+                }
             // Set the min, max boundaries of the thumb box.
             // I'm cheating by using the height of the seek bar as the width of the box.
             val halfHeight: Int = bar.height / 2
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
new file mode 100644
index 0000000..1a10b18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.recommendation
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.ui.IlluminationDrawable
+import com.android.systemui.util.animation.TransitionLayout
+
+private const val TAG = "RecommendationViewHolder"
+
+/** ViewHolder for a Smartspace media recommendation. */
+class RecommendationViewHolder private constructor(itemView: View) {
+
+    val recommendations = itemView as TransitionLayout
+
+    // Recommendation screen
+    val cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
+    val mediaCoverItems =
+        listOf<ImageView>(
+            itemView.requireViewById(R.id.media_cover1),
+            itemView.requireViewById(R.id.media_cover2),
+            itemView.requireViewById(R.id.media_cover3)
+        )
+    val mediaCoverContainers =
+        listOf<ViewGroup>(
+            itemView.requireViewById(R.id.media_cover1_container),
+            itemView.requireViewById(R.id.media_cover2_container),
+            itemView.requireViewById(R.id.media_cover3_container)
+        )
+    val mediaTitles: List<TextView> =
+        listOf(
+            itemView.requireViewById(R.id.media_title1),
+            itemView.requireViewById(R.id.media_title2),
+            itemView.requireViewById(R.id.media_title3)
+        )
+    val mediaSubtitles: List<TextView> =
+        listOf(
+            itemView.requireViewById(R.id.media_subtitle1),
+            itemView.requireViewById(R.id.media_subtitle2),
+            itemView.requireViewById(R.id.media_subtitle3)
+        )
+
+    val gutsViewHolder = GutsViewHolder(itemView)
+
+    init {
+        (recommendations.background as IlluminationDrawable).let { background ->
+            mediaCoverContainers.forEach { background.registerLightSource(it) }
+            background.registerLightSource(gutsViewHolder.cancel)
+            background.registerLightSource(gutsViewHolder.dismiss)
+            background.registerLightSource(gutsViewHolder.settings)
+        }
+    }
+
+    fun marquee(start: Boolean, delay: Long) {
+        gutsViewHolder.marquee(start, delay, TAG)
+    }
+
+    companion object {
+        /**
+         * Creates a RecommendationViewHolder.
+         *
+         * @param inflater LayoutInflater to use to inflate the layout.
+         * @param parent Parent of inflated view.
+         */
+        @JvmStatic
+        fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
+            val itemView =
+                inflater.inflate(
+                    R.layout.media_smartspace_recommendations,
+                    parent,
+                    false /* attachToRoot */
+                )
+            // Because this media view (a TransitionLayout) is used to measure and layout the views
+            // in various states before being attached to its parent, we can't depend on the default
+            // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
+            itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+            return RecommendationViewHolder(itemView)
+        }
+
+        // Res Ids for the control components on the recommendation view.
+        val controlsIds =
+            setOf(
+                R.id.recommendation_card_icon,
+                R.id.media_cover1,
+                R.id.media_cover2,
+                R.id.media_cover3,
+                R.id.media_cover1_container,
+                R.id.media_cover2_container,
+                R.id.media_cover3_container,
+                R.id.media_title1,
+                R.id.media_title2,
+                R.id.media_title3,
+                R.id.media_subtitle1,
+                R.id.media_subtitle2,
+                R.id.media_subtitle3
+            )
+
+        val mediaTitlesAndSubtitlesIds =
+            setOf(
+                R.id.media_title1,
+                R.id.media_title2,
+                R.id.media_title3,
+                R.id.media_subtitle1,
+                R.id.media_subtitle2,
+                R.id.media_subtitle3
+            )
+
+        val mediaContainersIds =
+            setOf(
+                R.id.media_cover1_container,
+                R.id.media_cover2_container,
+                R.id.media_cover3_container
+            )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
index c8f17d9..1df42c6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaData.kt
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.recommendation
 
 import android.app.smartspace.SmartspaceAction
 import android.content.Context
@@ -22,55 +22,41 @@
 import android.content.pm.PackageManager
 import android.text.TextUtils
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import com.android.internal.logging.InstanceId
-import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME
+
+@VisibleForTesting const val KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME"
 
 /** State of a Smartspace media recommendations view. */
 data class SmartspaceMediaData(
-    /**
-     * Unique id of a Smartspace media target.
-     */
+    /** Unique id of a Smartspace media target. */
     val targetId: String,
-    /**
-     * Indicates if the status is active.
-     */
+    /** Indicates if the status is active. */
     val isActive: Boolean,
-    /**
-     * Package name of the media recommendations' provider-app.
-     */
+    /** Package name of the media recommendations' provider-app. */
     val packageName: String,
-    /**
-     * Action to perform when the card is tapped. Also contains the target's extra info.
-     */
+    /** Action to perform when the card is tapped. Also contains the target's extra info. */
     val cardAction: SmartspaceAction?,
-    /**
-     * List of media recommendations.
-     */
+    /** List of media recommendations. */
     val recommendations: List<SmartspaceAction>,
-    /**
-     * Intent for the user's initiated dismissal.
-     */
+    /** Intent for the user's initiated dismissal. */
     val dismissIntent: Intent?,
-    /**
-     * The timestamp in milliseconds that headphone is connected.
-     */
+    /** The timestamp in milliseconds that headphone is connected. */
     val headphoneConnectionTimeMillis: Long,
-    /**
-     * Instance ID for [MediaUiEventLogger]
-     */
+    /** Instance ID for [MediaUiEventLogger] */
     val instanceId: InstanceId
 ) {
     /**
      * Indicates if all the data is valid.
      *
      * TODO(b/230333302): Make MediaControlPanel more flexible so that we can display fewer than
+     * ```
      *     [NUM_REQUIRED_RECOMMENDATIONS].
+     * ```
      */
     fun isValid() = getValidRecommendations().size >= NUM_REQUIRED_RECOMMENDATIONS
 
-    /**
-     * Returns the list of [recommendations] that have valid data.
-     */
+    /** Returns the list of [recommendations] that have valid data. */
     fun getValidRecommendations() = recommendations.filter { it.icon != null }
 
     /** Returns the upstream app name if available. */
@@ -89,9 +75,10 @@
         Log.w(
             TAG,
             "Package $packageName does not have a main launcher activity. " +
-                    "Fallback to full app name")
+                "Fallback to full app name"
+        )
         return try {
-            val applicationInfo = packageManager.getApplicationInfo(packageName,  /* flags= */ 0)
+            val applicationInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0)
             packageManager.getApplicationLabel(applicationInfo)
         } catch (e: PackageManager.NameNotFoundException) {
             null
diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt
index 140a1fe..a7ed69a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaDataProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataProvider.kt
@@ -1,4 +1,20 @@
-package com.android.systemui.media
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.recommendation
 
 import android.app.smartspace.SmartspaceTarget
 import android.util.Log
@@ -23,7 +39,7 @@
         smartspaceMediaTargetListeners.remove(smartspaceTargetListener)
     }
 
-    /** Updates Smartspace data and propagates it to any listeners.  */
+    /** Updates Smartspace data and propagates it to any listeners. */
     override fun onTargetsAvailable(targets: List<SmartspaceTarget>) {
         // Filter out non-media targets.
         val mediaTargets = mutableListOf<SmartspaceTarget>()
diff --git a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
index 94a0835..ff763d8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/LocalMediaManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/LocalMediaManagerFactory.kt
@@ -14,20 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.content.Context
-
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.media.InfoMediaManager
 import com.android.settingslib.media.LocalMediaManager
-
 import javax.inject.Inject
 
-/**
- * Factory to create [LocalMediaManager] objects.
- */
-class LocalMediaManagerFactory @Inject constructor(
+/** Factory to create [LocalMediaManager] objects. */
+class LocalMediaManagerFactory
+@Inject
+constructor(
     private val context: Context,
     private val localBluetoothManager: LocalBluetoothManager?
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt
index 311973a..789ef40 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatest.kt
@@ -14,15 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import javax.inject.Inject
 
-/**
- * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events.
- */
-class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
-        MediaDeviceManager.Listener {
+/** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */
+class MediaDataCombineLatest @Inject constructor() :
+    MediaDataManager.Listener, MediaDeviceManager.Listener {
 
     private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
     private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
@@ -60,11 +61,7 @@
         listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
     }
 
-    override fun onMediaDeviceChanged(
-        key: String,
-        oldKey: String?,
-        data: MediaDeviceData?
-    ) {
+    override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) {
         if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
             entries[key] = entries.remove(oldKey)?.first to data
             update(key, oldKey)
@@ -83,9 +80,7 @@
      */
     fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
 
-    /**
-     * Remove a listener registered with addListener.
-     */
+    /** Remove a listener registered with addListener. */
     fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
 
     private fun update(key: String, oldKey: String?) {
@@ -93,18 +88,14 @@
         if (entry != null && device != null) {
             val data = entry.copy(device = device)
             val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
-                it.onMediaDataLoaded(key, oldKey, data)
-            }
+            listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) }
         }
     }
 
     private fun remove(key: String) {
         entries.remove(key)?.let {
             val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
-                it.onMediaDataRemoved(key)
-            }
+            listenersCopy.forEach { it.onMediaDataRemoved(key) }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
index e0c8d66..45b319b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataFilter.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.content.Context
 import android.os.SystemProperties
@@ -23,6 +23,9 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.settings.CurrentUserTracker
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.util.time.SystemClock
@@ -34,7 +37,8 @@
 
 private const val TAG = "MediaDataFilter"
 private const val DEBUG = true
-private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = ("com.google" +
+private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME =
+    ("com.google" +
         ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity")
 private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds"
 
@@ -43,8 +47,8 @@
  * available within this time window, smartspace recommendations will be shown instead.
  */
 @VisibleForTesting
-internal val SMARTSPACE_MAX_AGE = SystemProperties
-        .getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
+internal val SMARTSPACE_MAX_AGE =
+    SystemProperties.getLong("debug.sysui.smartspace_max_age", TimeUnit.MINUTES.toMillis(30))
 
 /**
  * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
@@ -54,7 +58,9 @@
  * This is added at the end of the pipeline since we may still need to handle callbacks from
  * background users (e.g. timeouts).
  */
-class MediaDataFilter @Inject constructor(
+class MediaDataFilter
+@Inject
+constructor(
     private val context: Context,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val broadcastSender: BroadcastSender,
@@ -76,12 +82,13 @@
     private var reactivatedKey: String? = null
 
     init {
-        userTracker = object : CurrentUserTracker(broadcastDispatcher) {
-            override fun onUserSwitched(newUserId: Int) {
-                // Post this so we can be sure lockscreenUserManager already got the broadcast
-                executor.execute { handleUserSwitched(newUserId) }
+        userTracker =
+            object : CurrentUserTracker(broadcastDispatcher) {
+                override fun onUserSwitched(newUserId: Int) {
+                    // Post this so we can be sure lockscreenUserManager already got the broadcast
+                    executor.execute { handleUserSwitched(newUserId) }
+                }
             }
-        }
         userTracker.startTracking()
     }
 
@@ -108,9 +115,7 @@
         userEntries.put(key, data)
 
         // Notify listeners
-        listeners.forEach {
-            it.onMediaDataLoaded(key, oldKey, data)
-        }
+        listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) }
     }
 
     override fun onSmartspaceMediaDataLoaded(
@@ -128,14 +133,11 @@
         smartspaceMediaData = data
 
         // Before forwarding the smartspace target, first check if we have recently inactive media
-        val sorted = userEntries.toSortedMap(compareBy {
-            userEntries.get(it)?.lastActive ?: -1
-        })
+        val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 })
         val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted)
         var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE
         data.cardAction?.let {
-            val smartspaceMaxAgeSeconds =
-                it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
+            val smartspaceMaxAgeSeconds = it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0)
             if (smartspaceMaxAgeSeconds > 0) {
                 smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds)
             }
@@ -152,13 +154,21 @@
                 Log.d(TAG, "reactivating $lastActiveKey instead of smartspace")
                 reactivatedKey = lastActiveKey
                 val mediaData = sorted.get(lastActiveKey)!!.copy(active = true)
-                logger.logRecommendationActivated(mediaData.appUid, mediaData.packageName,
-                    mediaData.instanceId)
+                logger.logRecommendationActivated(
+                    mediaData.appUid,
+                    mediaData.packageName,
+                    mediaData.instanceId
+                )
                 listeners.forEach {
-                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData,
-                            receivedSmartspaceCardLatency =
+                    it.onMediaDataLoaded(
+                        lastActiveKey,
+                        lastActiveKey,
+                        mediaData,
+                        receivedSmartspaceCardLatency =
                             (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
-                                    .toInt(), isSsReactivated = true)
+                                .toInt(),
+                        isSsReactivated = true
+                    )
                 }
             }
         } else {
@@ -170,8 +180,10 @@
             Log.d(TAG, "Invalid recommendation data. Skip showing the rec card")
             return
         }
-        logger.logRecommendationAdded(smartspaceMediaData.packageName,
-            smartspaceMediaData.instanceId)
+        logger.logRecommendationAdded(
+            smartspaceMediaData.packageName,
+            smartspaceMediaData.instanceId
+        )
         listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
     }
 
@@ -179,9 +191,7 @@
         allEntries.remove(key)
         userEntries.remove(key)?.let {
             // Only notify listeners if something actually changed
-            listeners.forEach {
-                it.onMediaDataRemoved(key)
-            }
+            listeners.forEach { it.onMediaDataRemoved(key) }
         }
     }
 
@@ -194,16 +204,17 @@
             // Notify listeners to update with actual active value
             userEntries.get(lastActiveKey)?.let { mediaData ->
                 listeners.forEach {
-                    it.onMediaDataLoaded(
-                            lastActiveKey, lastActiveKey, mediaData, immediately)
+                    it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately)
                 }
             }
         }
 
         if (smartspaceMediaData.isActive) {
-            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = smartspaceMediaData.targetId,
-                instanceId = smartspaceMediaData.instanceId)
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
         }
         listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
     }
@@ -218,25 +229,19 @@
         userEntries.clear()
         keyCopy.forEach {
             if (DEBUG) Log.d(TAG, "Removing $it after user change")
-            listenersCopy.forEach { listener ->
-                listener.onMediaDataRemoved(it)
-            }
+            listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) }
         }
 
         allEntries.forEach { (key, data) ->
             if (lockscreenUserManager.isCurrentProfile(data.userId)) {
                 if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
                 userEntries.put(key, data)
-                listenersCopy.forEach { listener ->
-                    listener.onMediaDataLoaded(key, null, data)
-                }
+                listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
             }
         }
     }
 
-    /**
-     * Invoked when the user has dismissed the media carousel
-     */
+    /** Invoked when the user has dismissed the media carousel */
     fun onSwipeToDismiss() {
         if (DEBUG) Log.d(TAG, "Media carousel swiped away")
         val mediaKeys = userEntries.keys.toSet()
@@ -247,55 +252,52 @@
         if (smartspaceMediaData.isActive) {
             val dismissIntent = smartspaceMediaData.dismissIntent
             if (dismissIntent == null) {
-                Log.w(TAG, "Cannot create dismiss action click action: " +
-                        "extras missing dismiss_intent.")
-            } else if (dismissIntent.getComponent() != null &&
-                    dismissIntent.getComponent().getClassName()
-                    == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) {
+                Log.w(
+                    TAG,
+                    "Cannot create dismiss action click action: " + "extras missing dismiss_intent."
+                )
+            } else if (
+                dismissIntent.getComponent() != null &&
+                    dismissIntent.getComponent().getClassName() ==
+                        EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME
+            ) {
                 // Dismiss the card Smartspace data through Smartspace trampoline activity.
                 context.startActivity(dismissIntent)
             } else {
                 broadcastSender.sendBroadcast(dismissIntent)
             }
-            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = smartspaceMediaData.targetId,
-                instanceId = smartspaceMediaData.instanceId)
-            mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId,
-                delay = 0L)
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
+            mediaDataManager.dismissSmartspaceRecommendation(
+                smartspaceMediaData.targetId,
+                delay = 0L
+            )
         }
     }
 
-    /**
-     * Are there any active media entries, including the recommendation?
-     */
-    fun hasActiveMediaOrRecommendation() = userEntries.any { it.value.active } ||
+    /** Are there any active media entries, including the recommendation? */
+    fun hasActiveMediaOrRecommendation() =
+        userEntries.any { it.value.active } ||
             (smartspaceMediaData.isActive &&
                 (smartspaceMediaData.isValid() || reactivatedKey != null))
 
-    /**
-     * Are there any media entries we should display?
-     */
-    fun hasAnyMediaOrRecommendation() = userEntries.isNotEmpty() ||
-            (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
+    /** Are there any media entries we should display? */
+    fun hasAnyMediaOrRecommendation() =
+        userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
 
-    /**
-     * Are there any media notifications active (excluding the recommendation)?
-     */
+    /** Are there any media notifications active (excluding the recommendation)? */
     fun hasActiveMedia() = userEntries.any { it.value.active }
 
-    /**
-     * Are there any media entries we should display (excluding the recommendation)?
-     */
+    /** Are there any media entries we should display (excluding the recommendation)? */
     fun hasAnyMedia() = userEntries.isNotEmpty()
 
-    /**
-     * Add a listener for filtered [MediaData] changes
-     */
+    /** Add a listener for filtered [MediaData] changes */
     fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
 
-    /**
-     * Remove a listener that was registered with addListener
-     */
+    /** Remove a listener that was registered with addListener */
     fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
 
     /**
@@ -315,8 +317,6 @@
 
         val now = systemClock.elapsedRealtime()
         val lastActiveKey = sortedEntries.lastKey() // most recently active
-        return sortedEntries.get(lastActiveKey)?.let {
-            now - it.lastActive
-        } ?: Long.MAX_VALUE
+        return sortedEntries.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
similarity index 68%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 896fb47..14dd990 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.app.Notification
 import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
@@ -57,6 +57,17 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaAction
+import com.android.systemui.media.controls.models.player.MediaButton
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.resume.MediaResumeListener
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
@@ -75,17 +86,19 @@
 import javax.inject.Inject
 
 // URI fields to try loading album art from
-private val ART_URIS = arrayOf(
+private val ART_URIS =
+    arrayOf(
         MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
         MediaMetadata.METADATA_KEY_ART_URI,
         MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI
-)
+    )
 
 private const val TAG = "MediaDataManager"
 private const val DEBUG = true
 private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent"
 
-private val LOADING = MediaData(
+private val LOADING =
+    MediaData(
         userId = -1,
         initialized = false,
         app = null,
@@ -102,37 +115,41 @@
         active = true,
         resumeAction = null,
         instanceId = InstanceId.fakeInstanceId(-1),
-        appUid = Process.INVALID_UID)
+        appUid = Process.INVALID_UID
+    )
 
 @VisibleForTesting
-internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData(
-    targetId = "INVALID",
-    isActive = false,
-    packageName = "INVALID",
-    cardAction = null,
-    recommendations = emptyList(),
-    dismissIntent = null,
-    headphoneConnectionTimeMillis = 0,
-    instanceId = InstanceId.fakeInstanceId(-1))
+internal val EMPTY_SMARTSPACE_MEDIA_DATA =
+    SmartspaceMediaData(
+        targetId = "INVALID",
+        isActive = false,
+        packageName = "INVALID",
+        cardAction = null,
+        recommendations = emptyList(),
+        dismissIntent = null,
+        headphoneConnectionTimeMillis = 0,
+        instanceId = InstanceId.fakeInstanceId(-1)
+    )
 
 fun isMediaNotification(sbn: StatusBarNotification): Boolean {
     return sbn.notification.isMediaNotification()
 }
 
 /**
- * Allow recommendations from smartspace to show in media controls.
- * Requires [Utils.useQsMediaPlayer] to be enabled.
- * On by default, but can be disabled by setting to 0
+ * Allow recommendations from smartspace to show in media controls. Requires
+ * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0
  */
 private fun allowMediaRecommendations(context: Context): Boolean {
-    val flag = Settings.Secure.getInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
+    val flag =
+        Settings.Secure.getInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
+        )
     return Utils.useQsMediaPlayer(context) && flag > 0
 }
 
-/**
- * A class that facilitates management and loading of Media Data, ready for binding.
- */
+/** A class that facilitates management and loading of Media Data, ready for binding. */
 @SysUISingleton
 class MediaDataManager(
     private val context: Context,
@@ -159,24 +176,24 @@
 
     companion object {
         // UI surface label for subscribing Smartspace updates.
-        @JvmField
-        val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
+        @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager"
 
         // Smartspace package name's extra key.
-        @JvmField
-        val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
+        @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"
 
         // Maximum number of actions allowed in compact view
-        @JvmField
-        val MAX_COMPACT_ACTIONS = 3
+        @JvmField val MAX_COMPACT_ACTIONS = 3
 
         // Maximum number of actions allowed in expanded view
-        @JvmField
-        val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
+        @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size
     }
 
-    private val themeText = com.android.settingslib.Utils.getColorAttr(context,
-            com.android.internal.R.attr.textColorPrimary).defaultColor
+    private val themeText =
+        com.android.settingslib.Utils.getColorAttr(
+                context,
+                com.android.internal.R.attr.textColorPrimary
+            )
+            .defaultColor
 
     // Internal listeners are part of the internal pipeline. External listeners (those registered
     // with [MediaDeviceManager.addListener]) receive events after they have propagated through
@@ -192,9 +209,7 @@
     private var smartspaceSession: SmartspaceSession? = null
     private var allowMediaRecommendations = allowMediaRecommendations(context)
 
-    /**
-     * Check whether this notification is an RCN
-     */
+    /** Check whether this notification is an RCN */
     private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean {
         return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE)
     }
@@ -219,29 +234,44 @@
         tunerService: TunerService,
         mediaFlags: MediaFlags,
         logger: MediaUiEventLogger
-    ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
-            broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
-            mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
-            activityStarter, smartspaceMediaDataProvider, Utils.useMediaResumption(context),
-            Utils.useQsMediaPlayer(context), clock, tunerService, mediaFlags, logger)
+    ) : this(
+        context,
+        backgroundExecutor,
+        foregroundExecutor,
+        mediaControllerFactory,
+        broadcastDispatcher,
+        dumpManager,
+        mediaTimeoutListener,
+        mediaResumeListener,
+        mediaSessionBasedFilter,
+        mediaDeviceManager,
+        mediaDataCombineLatest,
+        mediaDataFilter,
+        activityStarter,
+        smartspaceMediaDataProvider,
+        Utils.useMediaResumption(context),
+        Utils.useQsMediaPlayer(context),
+        clock,
+        tunerService,
+        mediaFlags,
+        logger
+    )
 
-    private val appChangeReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            when (intent.action) {
-                Intent.ACTION_PACKAGES_SUSPENDED -> {
-                    val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-                    packages?.forEach {
-                        removeAllForPackage(it)
+    private val appChangeReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                when (intent.action) {
+                    Intent.ACTION_PACKAGES_SUSPENDED -> {
+                        val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST)
+                        packages?.forEach { removeAllForPackage(it) }
                     }
-                }
-                Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_RESTARTED -> {
-                    intent.data?.encodedSchemeSpecificPart?.let {
-                        removeAllForPackage(it)
+                    Intent.ACTION_PACKAGE_REMOVED,
+                    Intent.ACTION_PACKAGE_RESTARTED -> {
+                        intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) }
                     }
                 }
             }
         }
-    }
 
     init {
         dumpManager.registerDumpable(TAG, this)
@@ -262,20 +292,23 @@
 
         // Set up links back into the pipeline for listeners that need to send events upstream.
         mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean ->
-            setTimedOut(key, timedOut) }
+            setTimedOut(key, timedOut)
+        }
         mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState ->
-            updateState(key, state) }
+            updateState(key, state)
+        }
         mediaResumeListener.setManager(this)
         mediaDataFilter.mediaDataManager = this
 
         val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
         broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
 
-        val uninstallFilter = IntentFilter().apply {
-            addAction(Intent.ACTION_PACKAGE_REMOVED)
-            addAction(Intent.ACTION_PACKAGE_RESTARTED)
-            addDataScheme("package")
-        }
+        val uninstallFilter =
+            IntentFilter().apply {
+                addAction(Intent.ACTION_PACKAGE_REMOVED)
+                addAction(Intent.ACTION_PACKAGE_RESTARTED)
+                addDataScheme("package")
+            }
         // BroadcastDispatcher does not allow filters with data schemes
         context.registerReceiver(appChangeReceiver, uninstallFilter)
 
@@ -283,8 +316,10 @@
         smartspaceMediaDataProvider.registerListener(this)
         val smartspaceManager: SmartspaceManager =
             context.getSystemService(SmartspaceManager::class.java)
-        smartspaceSession = smartspaceManager.createSmartspaceSession(
-            SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build())
+        smartspaceSession =
+            smartspaceManager.createSmartspaceSession(
+                SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build()
+            )
         smartspaceSession?.let {
             it.addOnTargetsAvailableListener(
                 // Use a new thread listening to Smartspace updates instead of using the existing
@@ -296,17 +331,24 @@
                 Executors.newCachedThreadPool(),
                 SmartspaceSession.OnTargetsAvailableListener { targets ->
                     smartspaceMediaDataProvider.onTargetsAvailable(targets)
-                })
+                }
+            )
         }
         smartspaceSession?.let { it.requestSmartspaceUpdate() }
-        tunerService.addTunable(object : TunerService.Tunable {
-            override fun onTuningChanged(key: String?, newValue: String?) {
-                allowMediaRecommendations = allowMediaRecommendations(context)
-                if (!allowMediaRecommendations) {
-                    dismissSmartspaceRecommendation(key = smartspaceMediaData.targetId, delay = 0L)
+        tunerService.addTunable(
+            object : TunerService.Tunable {
+                override fun onTuningChanged(key: String?, newValue: String?) {
+                    allowMediaRecommendations = allowMediaRecommendations(context)
+                    if (!allowMediaRecommendations) {
+                        dismissSmartspaceRecommendation(
+                            key = smartspaceMediaData.targetId,
+                            delay = 0L
+                        )
+                    }
                 }
-            }
-        }, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)
+            },
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION
+        )
     }
 
     fun destroy() {
@@ -321,10 +363,7 @@
             val oldKey = findExistingEntry(key, sbn.packageName)
             if (oldKey == null) {
                 val instanceId = logger.getNewInstanceId()
-                val temp = LOADING.copy(
-                    packageName = sbn.packageName,
-                    instanceId = instanceId
-                )
+                val temp = LOADING.copy(packageName = sbn.packageName, instanceId = instanceId)
                 mediaEntries.put(key, temp)
                 logEvent = true
             } else if (oldKey != key) {
@@ -342,9 +381,7 @@
     private fun removeAllForPackage(packageName: String) {
         Assert.isMainThread()
         val toRemove = mediaEntries.filter { it.value.packageName == packageName }
-        toRemove.forEach {
-            removeEntry(it.key)
-        }
+        toRemove.forEach { removeEntry(it.key) }
     }
 
     fun setResumeAction(key: String, action: Runnable?) {
@@ -366,32 +403,41 @@
         // Resume controls don't have a notification key, so store by package name instead
         if (!mediaEntries.containsKey(packageName)) {
             val instanceId = logger.getNewInstanceId()
-            val appUid = try {
-                context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
-            } catch (e: PackageManager.NameNotFoundException) {
-                Log.w(TAG, "Could not get app UID for $packageName", e)
-                Process.INVALID_UID
-            }
+            val appUid =
+                try {
+                    context.packageManager.getApplicationInfo(packageName, 0)?.uid!!
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.w(TAG, "Could not get app UID for $packageName", e)
+                    Process.INVALID_UID
+                }
 
-            val resumeData = LOADING.copy(
-                packageName = packageName,
-                resumeAction = action,
-                hasCheckedForResume = true,
-                instanceId = instanceId,
-                appUid = appUid
-            )
+            val resumeData =
+                LOADING.copy(
+                    packageName = packageName,
+                    resumeAction = action,
+                    hasCheckedForResume = true,
+                    instanceId = instanceId,
+                    appUid = appUid
+                )
             mediaEntries.put(packageName, resumeData)
             logger.logResumeMediaAdded(appUid, packageName, instanceId)
         }
         backgroundExecutor.execute {
-            loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent,
-                packageName)
+            loadMediaDataInBgForResumption(
+                userId,
+                desc,
+                action,
+                token,
+                appName,
+                appIntent,
+                packageName
+            )
         }
     }
 
     /**
-     * Check if there is an existing entry that matches the key or package name.
-     * Returns the key that matches, or null if not found.
+     * Check if there is an existing entry that matches the key or package name. Returns the key
+     * that matches, or null if not found.
      */
     private fun findExistingEntry(key: String, packageName: String): String? {
         if (mediaEntries.containsKey(key)) {
@@ -410,32 +456,24 @@
         oldKey: String?,
         logEvent: Boolean = false
     ) {
-        backgroundExecutor.execute {
-            loadMediaDataInBg(key, sbn, oldKey, logEvent)
-        }
+        backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, logEvent) }
     }
 
-    /**
-     * Add a listener for changes in this class
-     */
+    /** Add a listener for changes in this class */
     fun addListener(listener: Listener) {
         // mediaDataFilter is the current end of the internal pipeline. Register external
         // listeners as listeners to it.
         mediaDataFilter.addListener(listener)
     }
 
-    /**
-     * Remove a listener for changes in this class
-     */
+    /** Remove a listener for changes in this class */
     fun removeListener(listener: Listener) {
         // Since mediaDataFilter is the current end of the internal pipelie, external listeners
         // have been registered to it. So, they need to be removed from it too.
         mediaDataFilter.removeListener(listener)
     }
 
-    /**
-     * Add a listener for internal events.
-     */
+    /** Add a listener for internal events. */
     private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
 
     /**
@@ -483,8 +521,8 @@
     }
 
     /**
-     * Called whenever the player has been paused or stopped for a while, or swiped from QQS.
-     * This will make the player not active anymore, hiding it from QQS and Keyguard.
+     * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This
+     * will make the player not active anymore, hiding it from QQS and Keyguard.
      * @see MediaData.active
      */
     internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) {
@@ -506,9 +544,7 @@
         }
     }
 
-    /**
-     * Called when the player's [PlaybackState] has been updated with new actions and/or state
-     */
+    /** Called when the player's [PlaybackState] has been updated with new actions and/or state */
     private fun updateState(key: String, state: PlaybackState) {
         mediaEntries.get(key)?.let {
             val token = it.token
@@ -516,22 +552,23 @@
                 if (DEBUG) Log.d(TAG, "State updated, but token was null")
                 return
             }
-            val actions = createActionsFromState(it.packageName,
-                    mediaControllerFactory.create(it.token), UserHandle(it.userId))
+            val actions =
+                createActionsFromState(
+                    it.packageName,
+                    mediaControllerFactory.create(it.token),
+                    UserHandle(it.userId)
+                )
 
             // Control buttons
             // If flag is enabled and controller has a PlaybackState,
             // create actions from session info
             // otherwise, no need to update semantic actions.
-            val data = if (actions != null) {
-                it.copy(
-                        semanticActions = actions,
-                        isPlaying = isPlayingState(state.state))
-            } else {
-                it.copy(
-                        isPlaying = isPlayingState(state.state)
-                )
-            }
+            val data =
+                if (actions != null) {
+                    it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state))
+                } else {
+                    it.copy(isPlaying = isPlayingState(state.state))
+                }
             if (DEBUG) Log.d(TAG, "State updated outside of notification")
             onMediaDataLoaded(key, key, data)
         }
@@ -544,9 +581,7 @@
         notifyMediaDataRemoved(key)
     }
 
-    /**
-     * Dismiss a media entry. Returns false if the key was not found.
-     */
+    /** Dismiss a media entry. Returns false if the key was not found. */
     fun dismissMediaData(key: String, delay: Long): Boolean {
         val existed = mediaEntries[key] != null
         backgroundExecutor.execute {
@@ -564,9 +599,8 @@
     }
 
     /**
-     * Called whenever the recommendation has been expired, or swiped from QQS.
-     * This will make the recommendation view to not be shown anymore during this headphone
-     * connection session.
+     * Called whenever the recommendation has been expired, or swiped from QQS. This will make the
+     * recommendation view to not be shown anymore during this headphone connection session.
      */
     fun dismissSmartspaceRecommendation(key: String, delay: Long) {
         if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
@@ -576,13 +610,16 @@
 
         if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target")
         if (smartspaceMediaData.isActive) {
-            smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = smartspaceMediaData.targetId,
-                instanceId = smartspaceMediaData.instanceId)
+            smartspaceMediaData =
+                EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                    targetId = smartspaceMediaData.targetId,
+                    instanceId = smartspaceMediaData.instanceId
+                )
         }
         foregroundExecutor.executeDelayed(
-            { notifySmartspaceMediaDataRemoved(
-                smartspaceMediaData.targetId, immediately = true) }, delay)
+            { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) },
+            delay
+        )
     }
 
     private fun loadMediaDataInBgForResumption(
@@ -610,11 +647,12 @@
         if (artworkBitmap == null && desc.iconUri != null) {
             artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
         }
-        val artworkIcon = if (artworkBitmap != null) {
-            Icon.createWithBitmap(artworkBitmap)
-        } else {
-            null
-        }
+        val artworkIcon =
+            if (artworkBitmap != null) {
+                Icon.createWithBitmap(artworkBitmap)
+            } else {
+                null
+            }
 
         val currentEntry = mediaEntries.get(packageName)
         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
@@ -623,13 +661,34 @@
         val mediaAction = getResumeMediaAction(resumeAction)
         val lastActive = systemClock.elapsedRealtime()
         foregroundExecutor.execute {
-            onMediaDataLoaded(packageName, null, MediaData(userId, true, appName,
-                    null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
-                    MediaButton(playOrPause = mediaAction), packageName, token, appIntent,
-                    device = null, active = false,
-                    resumeAction = resumeAction, resumption = true, notificationKey = packageName,
-                    hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId,
-                    appUid = appUid))
+            onMediaDataLoaded(
+                packageName,
+                null,
+                MediaData(
+                    userId,
+                    true,
+                    appName,
+                    null,
+                    desc.subtitle,
+                    desc.title,
+                    artworkIcon,
+                    listOf(mediaAction),
+                    listOf(0),
+                    MediaButton(playOrPause = mediaAction),
+                    packageName,
+                    token,
+                    appIntent,
+                    device = null,
+                    active = false,
+                    resumeAction = resumeAction,
+                    resumption = true,
+                    notificationKey = packageName,
+                    hasCheckedForResume = true,
+                    lastActive = lastActive,
+                    instanceId = instanceId,
+                    appUid = appUid
+                )
+            )
         }
     }
 
@@ -639,8 +698,11 @@
         oldKey: String?,
         logEvent: Boolean = false
     ) {
-        val token = sbn.notification.extras.getParcelable(
-                Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java)
+        val token =
+            sbn.notification.extras.getParcelable(
+                Notification.EXTRA_MEDIA_SESSION,
+                MediaSession.Token::class.java
+            )
         if (token == null) {
             return
         }
@@ -648,10 +710,12 @@
         val metadata = mediaController.metadata
         val notif: Notification = sbn.notification
 
-        val appInfo = notif.extras.getParcelable(
-            Notification.EXTRA_BUILDER_APPLICATION_INFO,
-            ApplicationInfo::class.java
-        ) ?: getAppInfoFromPackage(sbn.packageName)
+        val appInfo =
+            notif.extras.getParcelable(
+                Notification.EXTRA_BUILDER_APPLICATION_INFO,
+                ApplicationInfo::class.java
+            )
+                ?: getAppInfoFromPackage(sbn.packageName)
 
         // Album art
         var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
@@ -661,11 +725,12 @@
         if (artworkBitmap == null) {
             artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART)
         }
-        val artWorkIcon = if (artworkBitmap == null) {
-            notif.getLargeIcon()
-        } else {
-            Icon.createWithBitmap(artworkBitmap)
-        }
+        val artWorkIcon =
+            if (artworkBitmap == null) {
+                notif.getLargeIcon()
+            } else {
+                Icon.createWithBitmap(artworkBitmap)
+            }
 
         // App name
         val appName = getAppName(sbn, appInfo)
@@ -694,17 +759,27 @@
             val extras = sbn.notification.extras
             val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null)
             val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1)
-            val deviceIntent = extras.getParcelable(
-                    Notification.EXTRA_MEDIA_REMOTE_INTENT, PendingIntent::class.java)
+            val deviceIntent =
+                extras.getParcelable(
+                    Notification.EXTRA_MEDIA_REMOTE_INTENT,
+                    PendingIntent::class.java
+                )
             Log.d(TAG, "$key is RCN for $deviceName")
 
             if (deviceName != null && deviceIcon > -1) {
                 // Name and icon must be present, but intent may be null
                 val enabled = deviceIntent != null && deviceIntent.isActivity
-                val deviceDrawable = Icon.createWithResource(sbn.packageName, deviceIcon)
+                val deviceDrawable =
+                    Icon.createWithResource(sbn.packageName, deviceIcon)
                         .loadDrawable(sbn.getPackageContext(context))
-                device = MediaDeviceData(enabled, deviceDrawable, deviceName, deviceIntent,
-                        showBroadcastButton = false)
+                device =
+                    MediaDeviceData(
+                        enabled,
+                        deviceDrawable,
+                        deviceName,
+                        deviceIntent,
+                        showBroadcastButton = false
+                    )
             }
         }
 
@@ -721,10 +796,13 @@
         }
 
         val playbackLocation =
-                if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
-                else if (mediaController.playbackInfo?.playbackType ==
-                        MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) MediaData.PLAYBACK_LOCAL
-                else MediaData.PLAYBACK_CAST_LOCAL
+            if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE
+            else if (
+                mediaController.playbackInfo?.playbackType ==
+                    MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL
+            )
+                MediaData.PLAYBACK_LOCAL
+            else MediaData.PLAYBACK_CAST_LOCAL
         val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null
 
         val currentEntry = mediaEntries.get(key)
@@ -742,13 +820,36 @@
             val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
             val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
             val active = mediaEntries[key]?.active ?: true
-            onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, appName,
-                    smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed,
-                    semanticActions, sbn.packageName, token, notif.contentIntent, device,
-                    active, resumeAction = resumeAction, playbackLocation = playbackLocation,
-                    notificationKey = key, hasCheckedForResume = hasCheckedForResume,
-                    isPlaying = isPlaying, isClearable = sbn.isClearable(),
-                    lastActive = lastActive, instanceId = instanceId, appUid = appUid))
+            onMediaDataLoaded(
+                key,
+                oldKey,
+                MediaData(
+                    sbn.normalizedUserId,
+                    true,
+                    appName,
+                    smallIcon,
+                    artist,
+                    song,
+                    artWorkIcon,
+                    actionIcons,
+                    actionsToShowCollapsed,
+                    semanticActions,
+                    sbn.packageName,
+                    token,
+                    notif.contentIntent,
+                    device,
+                    active,
+                    resumeAction = resumeAction,
+                    playbackLocation = playbackLocation,
+                    notificationKey = key,
+                    hasCheckedForResume = hasCheckedForResume,
+                    isPlaying = isPlaying,
+                    isClearable = sbn.isClearable(),
+                    lastActive = lastActive,
+                    instanceId = instanceId,
+                    appUid = appUid
+                )
+            )
         }
     }
 
@@ -774,27 +875,33 @@
         }
     }
 
-    /**
-     * Generate action buttons based on notification actions
-     */
-    private fun createActionsFromNotification(sbn: StatusBarNotification):
-            Pair<List<MediaAction>, List<Int>> {
+    /** Generate action buttons based on notification actions */
+    private fun createActionsFromNotification(
+        sbn: StatusBarNotification
+    ): Pair<List<MediaAction>, List<Int>> {
         val notif = sbn.notification
         val actionIcons: MutableList<MediaAction> = ArrayList()
         val actions = notif.actions
-        var actionsToShowCollapsed = notif.extras.getIntArray(
-            Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() ?: mutableListOf()
+        var actionsToShowCollapsed =
+            notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
+                ?: mutableListOf()
         if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
-            Log.e(TAG, "Too many compact actions for ${sbn.key}," +
-                "limiting to first $MAX_COMPACT_ACTIONS")
+            Log.e(
+                TAG,
+                "Too many compact actions for ${sbn.key}," +
+                    "limiting to first $MAX_COMPACT_ACTIONS"
+            )
             actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
         }
 
         if (actions != null) {
             for ((index, action) in actions.withIndex()) {
                 if (index == MAX_NOTIFICATION_ACTIONS) {
-                    Log.w(TAG, "Too many notification actions for ${sbn.key}," +
-                        " limiting to first $MAX_NOTIFICATION_ACTIONS")
+                    Log.w(
+                        TAG,
+                        "Too many notification actions for ${sbn.key}," +
+                            " limiting to first $MAX_NOTIFICATION_ACTIONS"
+                    )
                     break
                 }
                 if (action.getIcon() == null) {
@@ -802,33 +909,38 @@
                     actionsToShowCollapsed.remove(index)
                     continue
                 }
-                val runnable = if (action.actionIntent != null) {
-                    Runnable {
-                        if (action.actionIntent.isActivity) {
-                            activityStarter.startPendingIntentDismissingKeyguard(
-                                action.actionIntent)
-                        } else if (action.isAuthenticationRequired()) {
-                            activityStarter.dismissKeyguardThenExecute({
-                                var result = sendPendingIntent(action.actionIntent)
-                                result
-                            }, {}, true)
-                        } else {
-                            sendPendingIntent(action.actionIntent)
+                val runnable =
+                    if (action.actionIntent != null) {
+                        Runnable {
+                            if (action.actionIntent.isActivity) {
+                                activityStarter.startPendingIntentDismissingKeyguard(
+                                    action.actionIntent
+                                )
+                            } else if (action.isAuthenticationRequired()) {
+                                activityStarter.dismissKeyguardThenExecute(
+                                    {
+                                        var result = sendPendingIntent(action.actionIntent)
+                                        result
+                                    },
+                                    {},
+                                    true
+                                )
+                            } else {
+                                sendPendingIntent(action.actionIntent)
+                            }
                         }
+                    } else {
+                        null
                     }
-                } else {
-                    null
-                }
-                val mediaActionIcon = if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
-                    Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
-                } else {
-                    action.getIcon()
-                }.setTint(themeText).loadDrawable(context)
-                val mediaAction = MediaAction(
-                    mediaActionIcon,
-                    runnable,
-                    action.title,
-                    null)
+                val mediaActionIcon =
+                    if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) {
+                            Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
+                        } else {
+                            action.getIcon()
+                        }
+                        .setTint(themeText)
+                        .loadDrawable(context)
+                val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
                 actionIcons.add(mediaAction)
             }
         }
@@ -841,7 +953,9 @@
      * @param packageName Package name for the media app
      * @param controller MediaController for the current session
      * @return a Pair consisting of a list of media actions, and a list of ints representing which
+     * ```
      *      of those actions should be shown in the compact player
+     * ```
      */
     private fun createActionsFromState(
         packageName: String,
@@ -854,59 +968,69 @@
         }
 
         // First, check for standard actions
-        val playOrPause = if (isConnectingState(state.state)) {
-            // Spinner needs to be animating to render anything. Start it here.
-            val drawable = context.getDrawable(
-                com.android.internal.R.drawable.progress_small_material)
-            (drawable as Animatable).start()
-            MediaAction(
-                drawable,
-                null, // no action to perform when clicked
-                context.getString(R.string.controls_media_button_connecting),
-                context.getDrawable(R.drawable.ic_media_connecting_container),
-                // Specify a rebind id to prevent the spinner from restarting on later binds.
-                com.android.internal.R.drawable.progress_small_material
-            )
-        } else if (isPlayingState(state.state)) {
-            getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
-        } else {
-            getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
-        }
-        val prevButton = getStandardAction(controller, state.actions,
-            PlaybackState.ACTION_SKIP_TO_PREVIOUS)
-        val nextButton = getStandardAction(controller, state.actions,
-            PlaybackState.ACTION_SKIP_TO_NEXT)
+        val playOrPause =
+            if (isConnectingState(state.state)) {
+                // Spinner needs to be animating to render anything. Start it here.
+                val drawable =
+                    context.getDrawable(com.android.internal.R.drawable.progress_small_material)
+                (drawable as Animatable).start()
+                MediaAction(
+                    drawable,
+                    null, // no action to perform when clicked
+                    context.getString(R.string.controls_media_button_connecting),
+                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    // Specify a rebind id to prevent the spinner from restarting on later binds.
+                    com.android.internal.R.drawable.progress_small_material
+                )
+            } else if (isPlayingState(state.state)) {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
+            } else {
+                getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
+            }
+        val prevButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
+        val nextButton =
+            getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
 
         // Then, create a way to build any custom actions that will be needed
-        val customActions = state.customActions.asSequence().filterNotNull().map {
-            getCustomAction(state, packageName, controller, it)
-        }.iterator()
+        val customActions =
+            state.customActions
+                .asSequence()
+                .filterNotNull()
+                .map { getCustomAction(state, packageName, controller, it) }
+                .iterator()
         fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
 
         // Finally, assign the remaining button slots: play/pause A B C D
         // A = previous, else custom action (if not reserved)
         // B = next, else custom action (if not reserved)
         // C and D are always custom actions
-        val reservePrev = controller.extras?.getBoolean(
-            MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV) == true
-        val reserveNext = controller.extras?.getBoolean(
-            MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT) == true
+        val reservePrev =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
+            ) == true
+        val reserveNext =
+            controller.extras?.getBoolean(
+                MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
+            ) == true
 
-        val prevOrCustom = if (prevButton != null) {
-            prevButton
-        } else if (!reservePrev) {
-            nextCustomAction()
-        } else {
-            null
-        }
+        val prevOrCustom =
+            if (prevButton != null) {
+                prevButton
+            } else if (!reservePrev) {
+                nextCustomAction()
+            } else {
+                null
+            }
 
-        val nextOrCustom = if (nextButton != null) {
-            nextButton
-        } else if (!reserveNext) {
-            nextCustomAction()
-        } else {
-            null
-        }
+        val nextOrCustom =
+            if (nextButton != null) {
+                nextButton
+            } else if (!reserveNext) {
+                nextCustomAction()
+            } else {
+                null
+            }
 
         return MediaButton(
             playOrPause,
@@ -925,11 +1049,14 @@
      * @param controller MediaController for the session
      * @param stateActions The actions included with the session's [PlaybackState]
      * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
+     * ```
      *      [PlaybackState.ACTION_PLAY]
      *      [PlaybackState.ACTION_PAUSE]
      *      [PlaybackState.ACTION_SKIP_TO_PREVIOUS]
      *      [PlaybackState.ACTION_SKIP_TO_NEXT]
-     * @return A [MediaAction] with correct values set, or null if the state doesn't support it
+     * @return
+     * ```
+     * A [MediaAction] with correct values set, or null if the state doesn't support it
      */
     private fun getStandardAction(
         controller: MediaController,
@@ -977,20 +1104,18 @@
         }
     }
 
-    /**
-     * Check whether the actions from a [PlaybackState] include a specific action
-     */
+    /** Check whether the actions from a [PlaybackState] include a specific action */
     private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
-        if ((action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
-                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)) {
+        if (
+            (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
+                (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
+        ) {
             return true
         }
         return (stateActions and action != 0L)
     }
 
-    /**
-     * Get a [MediaAction] representing a [PlaybackState.CustomAction]
-     */
+    /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
     private fun getCustomAction(
         state: PlaybackState,
         packageName: String,
@@ -1005,9 +1130,7 @@
         )
     }
 
-    /**
-     * Load a bitmap from the various Art metadata URIs
-     */
+    /** Load a bitmap from the various Art metadata URIs */
     private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? {
         for (uri in ART_URIS) {
             val uriString = metadata.getString(uri)
@@ -1042,16 +1165,18 @@
             return null
         }
 
-        if (!uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
+        if (
+            !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) &&
                 !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) &&
-                !uri.scheme.equals(ContentResolver.SCHEME_FILE)) {
+                !uri.scheme.equals(ContentResolver.SCHEME_FILE)
+        ) {
             return null
         }
 
         val source = ImageDecoder.createSource(context.getContentResolver(), uri)
         return try {
-            ImageDecoder.decodeBitmap(source) {
-                decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
+                decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
             }
         } catch (e: IOException) {
             Log.e(TAG, "Unable to load bitmap", e)
@@ -1065,25 +1190,23 @@
     private fun getResumeMediaAction(action: Runnable): MediaAction {
         return MediaAction(
             Icon.createWithResource(context, R.drawable.ic_media_play)
-                .setTint(themeText).loadDrawable(context),
+                .setTint(themeText)
+                .loadDrawable(context),
             action,
             context.getString(R.string.controls_media_resume),
             context.getDrawable(R.drawable.ic_media_play_container)
         )
     }
 
-    fun onMediaDataLoaded(
-        key: String,
-        oldKey: String?,
-        data: MediaData
-    ) = traceSection("MediaDataManager#onMediaDataLoaded") {
-        Assert.isMainThread()
-        if (mediaEntries.containsKey(key)) {
-            // Otherwise this was removed already
-            mediaEntries.put(key, data)
-            notifyMediaDataLoaded(key, oldKey, data)
+    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) =
+        traceSection("MediaDataManager#onMediaDataLoaded") {
+            Assert.isMainThread()
+            if (mediaEntries.containsKey(key)) {
+                // Otherwise this was removed already
+                mediaEntries.put(key, data)
+                notifyMediaDataLoaded(key, oldKey, data)
+            }
         }
-    }
 
     override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) {
         if (!allowMediaRecommendations) {
@@ -1100,9 +1223,11 @@
                 if (DEBUG) {
                     Log.d(TAG, "Set Smartspace media to be inactive for the data update")
                 }
-                smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                    targetId = smartspaceMediaData.targetId,
-                    instanceId = smartspaceMediaData.instanceId)
+                smartspaceMediaData =
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = smartspaceMediaData.targetId,
+                        instanceId = smartspaceMediaData.instanceId
+                    )
                 notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
             }
             1 -> {
@@ -1113,15 +1238,16 @@
                 }
                 if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
                 smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
-                notifySmartspaceMediaDataLoaded(
-                    smartspaceMediaData.targetId, smartspaceMediaData)
+                notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
             }
             else -> {
                 // There should NOT be more than 1 Smartspace media update. When it happens, it
                 // indicates a bad state or an error. Reset the status accordingly.
                 Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
                 notifySmartspaceMediaDataRemoved(
-                    smartspaceMediaData.targetId, false /* immediately */)
+                    smartspaceMediaData.targetId,
+                    false /* immediately */
+                )
                 smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
             }
         }
@@ -1134,10 +1260,17 @@
             Log.d(TAG, "Not removing $key because resumable")
             // Move to resume key (aka package name) if that key doesn't already exist.
             val resumeAction = getResumeMediaAction(removed.resumeAction!!)
-            val updated = removed.copy(token = null, actions = listOf(resumeAction),
+            val updated =
+                removed.copy(
+                    token = null,
+                    actions = listOf(resumeAction),
                     semanticActions = MediaButton(playOrPause = resumeAction),
-                    actionsToShowInCompact = listOf(0), active = false, resumption = true,
-                    isPlaying = false, isClearable = true)
+                    actionsToShowInCompact = listOf(0),
+                    active = false,
+                    resumption = true,
+                    isPlaying = false,
+                    isClearable = true
+                )
             val pkg = removed.packageName
             val migrate = mediaEntries.put(pkg, updated) == null
             // Notify listeners of "new" controls when migrating or removed and update when not
@@ -1179,33 +1312,27 @@
         }
     }
 
-    /**
-     * Invoked when the user has dismissed the media carousel
-     */
+    /** Invoked when the user has dismissed the media carousel */
     fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
 
-    /**
-     * Are there any media notifications active, including the recommendations?
-     */
+    /** Are there any media notifications active, including the recommendations? */
     fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation()
 
     /**
      * Are there any media entries we should display, including the recommendations?
-     * If resumption is enabled, this will include inactive players
-     * If resumption is disabled, we only want to show active players
+     * - If resumption is enabled, this will include inactive players
+     * - If resumption is disabled, we only want to show active players
      */
     fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation()
 
-    /**
-     * Are there any resume media notifications active, excluding the recommendations?
-     */
+    /** Are there any resume media notifications active, excluding the recommendations? */
     fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
 
     /**
-    * Are there any resume media notifications active, excluding the recommendations?
-    * If resumption is enabled, this will include inactive players
-    * If resumption is disabled, we only want to show active players
-    */
+     * Are there any resume media notifications active, excluding the recommendations?
+     * - If resumption is enabled, this will include inactive players
+     * - If resumption is disabled, we only want to show active players
+     */
     fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
 
     interface Listener {
@@ -1275,10 +1402,9 @@
     ): SmartspaceMediaData {
         var dismissIntent: Intent? = null
         if (target.baseAction != null && target.baseAction.extras != null) {
-            dismissIntent = target
-                .baseAction
-                .extras
-                .getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent?
+            dismissIntent =
+                target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
+                    as Intent?
         }
         packageName(target)?.let {
             return SmartspaceMediaData(
@@ -1289,14 +1415,16 @@
                 recommendations = target.iconGrid,
                 dismissIntent = dismissIntent,
                 headphoneConnectionTimeMillis = target.creationTimeMillis,
-                instanceId = logger.getNewInstanceId())
+                instanceId = logger.getNewInstanceId()
+            )
         }
-        return EMPTY_SMARTSPACE_MEDIA_DATA
-            .copy(targetId = target.smartspaceTargetId,
-                    isActive = isActive,
-                    dismissIntent = dismissIntent,
-                    headphoneConnectionTimeMillis = target.creationTimeMillis,
-                    instanceId = logger.getNewInstanceId())
+        return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+            targetId = target.smartspaceTargetId,
+            isActive = isActive,
+            dismissIntent = dismissIntent,
+            headphoneConnectionTimeMillis = target.creationTimeMillis,
+            instanceId = logger.getNewInstanceId()
+        )
     }
 
     private fun packageName(target: SmartspaceTarget): String? {
@@ -1308,8 +1436,9 @@
         for (recommendation in recommendationList) {
             val extras = recommendation.extras
             extras?.let {
-                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let {
-                    packageName -> return packageName }
+                it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName ->
+                    return packageName
+                }
             }
         }
         Log.w(TAG, "No valid package name is provided.")
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
index b3a4ddf..6a512be 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.bluetooth.BluetoothLeBroadcast
 import android.bluetooth.BluetoothLeBroadcastMetadata
@@ -36,6 +36,10 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaDataUtils
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -47,10 +51,10 @@
 private const val TAG = "MediaDeviceManager"
 private const val DEBUG = true
 
-/**
- * Provides information about the route (ie. device) where playback is occurring.
- */
-class MediaDeviceManager @Inject constructor(
+/** Provides information about the route (ie. device) where playback is occurring. */
+class MediaDeviceManager
+@Inject
+constructor(
     private val context: Context,
     private val controllerFactory: MediaControllerFactory,
     private val localMediaManagerFactory: LocalMediaManagerFactory,
@@ -70,14 +74,10 @@
         dumpManager.registerDumpable(javaClass.name, this)
     }
 
-    /**
-     * Add a listener for changes to the media route (ie. device).
-     */
+    /** Add a listener for changes to the media route (ie. device). */
     fun addListener(listener: Listener) = listeners.add(listener)
 
-    /**
-     * Remove a listener that has been registered with addListener.
-     */
+    /** Remove a listener that has been registered with addListener. */
     fun removeListener(listener: Listener) = listeners.remove(listener)
 
     override fun onMediaDataLoaded(
@@ -101,19 +101,11 @@
                 processDevice(key, oldKey, data.device)
                 return
             }
-            val controller = data.token?.let {
-                controllerFactory.create(it)
-            }
+            val controller = data.token?.let { controllerFactory.create(it) }
             val localMediaManager = localMediaManagerFactory.create(data.packageName)
             val muteAwaitConnectionManager =
-                    muteAwaitConnectionManagerFactory.create(localMediaManager)
-            entry = Entry(
-                key,
-                oldKey,
-                controller,
-                localMediaManager,
-                muteAwaitConnectionManager
-            )
+                muteAwaitConnectionManagerFactory.create(localMediaManager)
+            entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
             entries[key] = entry
             entry.start()
         }
@@ -122,11 +114,7 @@
     override fun onMediaDataRemoved(key: String) {
         val token = entries.remove(key)
         token?.stop()
-        token?.let {
-            listeners.forEach {
-                it.onKeyRemoved(key)
-            }
-        }
+        token?.let { listeners.forEach { it.onKeyRemoved(key) } }
     }
 
     override fun dump(pw: PrintWriter, args: Array<String>) {
@@ -141,9 +129,7 @@
 
     @MainThread
     private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
-        listeners.forEach {
-            it.onMediaDeviceChanged(key, oldKey, device)
-        }
+        listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
     }
 
     interface Listener {
@@ -159,8 +145,10 @@
         val controller: MediaController?,
         val localMediaManager: LocalMediaManager,
         val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
-    ) : LocalMediaManager.DeviceCallback, MediaController.Callback(),
-            BluetoothLeBroadcast.Callback {
+    ) :
+        LocalMediaManager.DeviceCallback,
+        MediaController.Callback(),
+        BluetoothLeBroadcast.Callback {
 
         val token
             get() = controller?.sessionToken
@@ -171,54 +159,52 @@
                 val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
                 if (!started || !sameWithoutIcon) {
                     field = value
-                    fgExecutor.execute {
-                        processDevice(key, oldKey, value)
-                    }
+                    fgExecutor.execute { processDevice(key, oldKey, value) }
                 }
             }
         // A device that is not yet connected but is expected to connect imminently. Because it's
         // expected to connect imminently, it should be displayed as the current device.
         private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
         private var broadcastDescription: String? = null
-        private val configListener = object : ConfigurationController.ConfigurationListener {
-            override fun onLocaleListChanged() {
-                updateCurrent()
+        private val configListener =
+            object : ConfigurationController.ConfigurationListener {
+                override fun onLocaleListChanged() {
+                    updateCurrent()
+                }
             }
-        }
 
         @AnyThread
-        fun start() = bgExecutor.execute {
-            if (!started) {
-                localMediaManager.registerCallback(this)
-                localMediaManager.startScan()
-                muteAwaitConnectionManager?.startListening()
-                playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
-                controller?.registerCallback(this)
-                updateCurrent()
-                started = true
-                configurationController.addCallback(configListener)
+        fun start() =
+            bgExecutor.execute {
+                if (!started) {
+                    localMediaManager.registerCallback(this)
+                    localMediaManager.startScan()
+                    muteAwaitConnectionManager?.startListening()
+                    playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
+                    controller?.registerCallback(this)
+                    updateCurrent()
+                    started = true
+                    configurationController.addCallback(configListener)
+                }
             }
-        }
 
         @AnyThread
-        fun stop() = bgExecutor.execute {
-            if (started) {
-                started = false
-                controller?.unregisterCallback(this)
-                localMediaManager.stopScan()
-                localMediaManager.unregisterCallback(this)
-                muteAwaitConnectionManager?.stopListening()
-                configurationController.removeCallback(configListener)
+        fun stop() =
+            bgExecutor.execute {
+                if (started) {
+                    started = false
+                    controller?.unregisterCallback(this)
+                    localMediaManager.stopScan()
+                    localMediaManager.unregisterCallback(this)
+                    muteAwaitConnectionManager?.stopListening()
+                    configurationController.removeCallback(configListener)
+                }
             }
-        }
 
         fun dump(pw: PrintWriter) {
-            val routingSession = controller?.let {
-                mr2manager.getRoutingSessionForMediaController(it)
-            }
-            val selectedRoutes = routingSession?.let {
-                mr2manager.getSelectedRoutes(it)
-            }
+            val routingSession =
+                controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
+            val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
             with(pw) {
                 println("    current device is ${current?.name}")
                 val type = controller?.playbackInfo?.playbackType
@@ -238,14 +224,11 @@
             updateCurrent()
         }
 
-        override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute {
-            updateCurrent()
-        }
+        override fun onDeviceListUpdate(devices: List<MediaDevice>?) =
+            bgExecutor.execute { updateCurrent() }
 
         override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
-            bgExecutor.execute {
-                updateCurrent()
-            }
+            bgExecutor.execute { updateCurrent() }
         }
 
         override fun onAboutToConnectDeviceAdded(
@@ -253,14 +236,17 @@
             deviceName: String,
             deviceIcon: Drawable?
         ) {
-            aboutToConnectDeviceOverride = AboutToConnectDevice(
-                fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
-                backupMediaDeviceData = MediaDeviceData(
-                        /* enabled */ enabled = true,
-                        /* icon */ deviceIcon,
-                        /* name */ deviceName,
-                        /* showBroadcastButton */ showBroadcastButton = false)
-            )
+            aboutToConnectDeviceOverride =
+                AboutToConnectDevice(
+                    fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
+                    backupMediaDeviceData =
+                        MediaDeviceData(
+                            /* enabled */ enabled = true,
+                            /* icon */ deviceIcon,
+                            /* name */ deviceName,
+                            /* showBroadcastButton */ showBroadcastButton = false
+                        )
+                )
             updateCurrent()
         }
 
@@ -287,8 +273,11 @@
             metadata: BluetoothLeBroadcastMetadata
         ) {
             if (DEBUG) {
-                Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
-                        "metadata = $metadata")
+                Log.d(
+                    TAG,
+                    "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
+                        "metadata = $metadata"
+                )
             }
             updateCurrent()
         }
@@ -315,8 +304,10 @@
 
         override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
             if (DEBUG) {
-                Log.d(TAG, "onBroadcastUpdateFailed(), reason = $reason , " +
-                        "broadcastId = $broadcastId")
+                Log.d(
+                    TAG,
+                    "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId"
+                )
             }
         }
 
@@ -327,34 +318,45 @@
         @WorkerThread
         private fun updateCurrent() {
             if (isLeAudioBroadcastEnabled()) {
-                current = MediaDeviceData(
+                current =
+                    MediaDeviceData(
                         /* enabled */ true,
                         /* icon */ context.getDrawable(R.drawable.settings_input_antenna),
                         /* name */ broadcastDescription,
                         /* intent */ null,
-                        /* showBroadcastButton */ showBroadcastButton = true)
+                        /* showBroadcastButton */ showBroadcastButton = true
+                    )
             } else {
                 val aboutToConnect = aboutToConnectDeviceOverride
-                if (aboutToConnect != null &&
+                if (
+                    aboutToConnect != null &&
                         aboutToConnect.fullMediaDevice == null &&
-                        aboutToConnect.backupMediaDeviceData != null) {
+                        aboutToConnect.backupMediaDeviceData != null
+                ) {
                     // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
                     current = aboutToConnect.backupMediaDeviceData
                     return
                 }
-                val device = aboutToConnect?.fullMediaDevice
-                        ?: localMediaManager.currentConnectedDevice
+                val device =
+                    aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
                 val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
 
                 // If we have a controller but get a null route, then don't trust the device
                 val enabled = device != null && (controller == null || route != null)
-                val name = if (controller == null || route != null) {
-                    route?.name?.toString() ?: device?.name
-                } else {
-                    null
-                }
-                current = MediaDeviceData(enabled, device?.iconWithoutBackground, name,
-                        id = device?.id, showBroadcastButton = false)
+                val name =
+                    if (controller == null || route != null) {
+                        route?.name?.toString() ?: device?.name
+                    } else {
+                        null
+                    }
+                current =
+                    MediaDeviceData(
+                        enabled,
+                        device?.iconWithoutBackground,
+                        name,
+                        id = device?.id,
+                        showBroadcastButton = false
+                    )
             }
         }
 
@@ -384,13 +386,16 @@
             // unexpected result.
             // Check the current media app's name is the same with current broadcast app's name
             // or not.
-            var mediaApp = MediaDataUtils.getAppLabel(
-                    context, localMediaManager.packageName,
-                    context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name))
+            var mediaApp =
+                MediaDataUtils.getAppLabel(
+                    context,
+                    localMediaManager.packageName,
+                    context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
+                )
             var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
             if (isCurrentBroadcastedApp) {
-                broadcastDescription = context.getString(
-                        R.string.broadcasting_description_is_broadcasting)
+                broadcastDescription =
+                    context.getString(R.string.broadcasting_description_is_broadcasting)
             } else {
                 broadcastDescription = currentBroadcastedApp
             }
@@ -403,9 +408,9 @@
  * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
  *
  * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
- *   non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
+ * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
  * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
- *   information required to display the device. Only use if [fullMediaDevice] is null.
+ * information required to display the device. Only use if [fullMediaDevice] is null.
  */
 private data class AboutToConnectDevice(
     val fullMediaDevice: MediaDevice? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
index 3179296..ab93b29 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilter.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.content.ComponentName
 import android.content.Context
@@ -25,6 +25,8 @@
 import android.util.Log
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -38,7 +40,9 @@
  * sessions. In this situation, there should only be a media object for the remote session. To
  * achieve this, update events for the local session need to be filtered.
  */
-class MediaSessionBasedFilter @Inject constructor(
+class MediaSessionBasedFilter
+@Inject
+constructor(
     context: Context,
     private val sessionManager: MediaSessionManager,
     @Main private val foregroundExecutor: Executor,
@@ -50,7 +54,7 @@
     // Keep track of MediaControllers for a given package to check if an app is casting and it
     // filter loaded events for local sessions.
     private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> =
-            LinkedHashMap()
+        LinkedHashMap()
 
     // Keep track of the key used for the session tokens. This information is used to know when to
     // dispatch a removed event so that a media object for a local session will be removed.
@@ -59,11 +63,12 @@
     // Keep track of which media session tokens have associated notifications.
     private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf()
 
-    private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener {
-        override fun onActiveSessionsChanged(controllers: List<MediaController>) {
-            handleControllersChanged(controllers)
+    private val sessionListener =
+        object : MediaSessionManager.OnActiveSessionsChangedListener {
+            override fun onActiveSessionsChanged(controllers: List<MediaController>) {
+                handleControllersChanged(controllers)
+            }
         }
-    }
 
     init {
         backgroundExecutor.execute {
@@ -73,14 +78,10 @@
         }
     }
 
-    /**
-     * Add a listener for filtered [MediaData] changes
-     */
+    /** Add a listener for filtered [MediaData] changes */
     fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
 
-    /**
-     * Remove a listener that was registered with addListener
-     */
+    /** Remove a listener that was registered with addListener */
     fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
 
     /**
@@ -100,31 +101,32 @@
         isSsReactivated: Boolean
     ) {
         backgroundExecutor.execute {
-            data.token?.let {
-                tokensWithNotifications.add(it)
-            }
+            data.token?.let { tokensWithNotifications.add(it) }
             val isMigration = oldKey != null && key != oldKey
             if (isMigration) {
                 keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
             }
             if (data.token != null) {
-                keyedTokens.get(key)?.let {
-                    tokens ->
-                    tokens.add(data.token)
-                } ?: run {
-                    val tokens = mutableSetOf(data.token)
-                    keyedTokens.put(key, tokens)
-                }
+                keyedTokens.get(key)?.let { tokens -> tokens.add(data.token) }
+                    ?: run {
+                        val tokens = mutableSetOf(data.token)
+                        keyedTokens.put(key, tokens)
+                    }
             }
             // Determine if an app is casting by checking if it has a session with playback type
             // PLAYBACK_TYPE_REMOTE.
-            val remoteControllers = packageControllers.get(data.packageName)?.filter {
-                it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
-            }
+            val remoteControllers =
+                packageControllers.get(data.packageName)?.filter {
+                    it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
+                }
             // Limiting search to only apps with a single remote session.
             val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
-            if (isMigration || remote == null || remote.sessionToken == data.token ||
-                    !tokensWithNotifications.contains(remote.sessionToken)) {
+            if (
+                isMigration ||
+                    remote == null ||
+                    remote.sessionToken == data.token ||
+                    !tokensWithNotifications.contains(remote.sessionToken)
+            ) {
                 // Not filtering in this case. Passing the event along to listeners.
                 dispatchMediaDataLoaded(key, oldKey, data, immediately)
             } else {
@@ -146,9 +148,7 @@
         data: SmartspaceMediaData,
         shouldPrioritize: Boolean
     ) {
-        backgroundExecutor.execute {
-            dispatchSmartspaceMediaDataLoaded(key, data)
-        }
+        backgroundExecutor.execute { dispatchSmartspaceMediaDataLoaded(key, data) }
     }
 
     override fun onMediaDataRemoved(key: String) {
@@ -160,9 +160,7 @@
     }
 
     override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-        backgroundExecutor.execute {
-            dispatchSmartspaceMediaDataRemoved(key, immediately)
-        }
+        backgroundExecutor.execute { dispatchSmartspaceMediaDataRemoved(key, immediately) }
     }
 
     private fun dispatchMediaDataLoaded(
@@ -177,9 +175,7 @@
     }
 
     private fun dispatchMediaDataRemoved(key: String) {
-        foregroundExecutor.execute {
-            listeners.toSet().forEach { it.onMediaDataRemoved(key) }
-        }
+        foregroundExecutor.execute { listeners.toSet().forEach { it.onMediaDataRemoved(key) } }
     }
 
     private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) {
@@ -196,15 +192,12 @@
 
     private fun handleControllersChanged(controllers: List<MediaController>) {
         packageControllers.clear()
-        controllers.forEach {
-            controller ->
-            packageControllers.get(controller.packageName)?.let {
-                tokens ->
-                tokens.add(controller)
-            } ?: run {
-                val tokens = mutableListOf(controller)
-                packageControllers.put(controller.packageName, tokens)
-            }
+        controllers.forEach { controller ->
+            packageControllers.get(controller.packageName)?.let { tokens -> tokens.add(controller) }
+                ?: run {
+                    val tokens = mutableListOf(controller)
+                    packageControllers.put(controller.packageName, tokens)
+                }
         }
         tokensWithNotifications.retainAll(controllers.map { it.sessionToken })
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
index 93a29ef..7f5c82f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.media.session.MediaController
 import android.media.session.PlaybackState
@@ -22,6 +22,8 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -31,18 +33,18 @@
 import javax.inject.Inject
 
 @VisibleForTesting
-val PAUSED_MEDIA_TIMEOUT = SystemProperties
-        .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
+val PAUSED_MEDIA_TIMEOUT =
+    SystemProperties.getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
 
 @VisibleForTesting
-val RESUME_MEDIA_TIMEOUT = SystemProperties
-        .getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
+val RESUME_MEDIA_TIMEOUT =
+    SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
 
-/**
- * Controller responsible for keeping track of playback states and expiring inactive streams.
- */
+/** Controller responsible for keeping track of playback states and expiring inactive streams. */
 @SysUISingleton
-class MediaTimeoutListener @Inject constructor(
+class MediaTimeoutListener
+@Inject
+constructor(
     private val mediaControllerFactory: MediaControllerFactory,
     @Main private val mainExecutor: DelayableExecutor,
     private val logger: MediaTimeoutLogger,
@@ -56,7 +58,9 @@
      * Callback representing that a media object is now expired:
      * @param key Media control unique identifier
      * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media,
+     * ```
      *                 or {@code RESUME_MEDIA_TIMEOUT} for resume media
+     * ```
      */
     lateinit var timeoutCallback: (String, Boolean) -> Unit
 
@@ -68,21 +72,25 @@
     lateinit var stateCallback: (String, PlaybackState) -> Unit
 
     init {
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onDozingChanged(isDozing: Boolean) {
-                if (!isDozing) {
-                    // Check whether any timeouts should have expired
-                    mediaListeners.forEach { (key, listener) ->
-                        if (listener.cancellation != null &&
-                                listener.expiration <= systemClock.elapsedRealtime()) {
-                            // We dozed too long - timeout now, and cancel the pending one
-                            listener.expireMediaTimeout(key, "timeout happened while dozing")
-                            listener.doTimeout()
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onDozingChanged(isDozing: Boolean) {
+                    if (!isDozing) {
+                        // Check whether any timeouts should have expired
+                        mediaListeners.forEach { (key, listener) ->
+                            if (
+                                listener.cancellation != null &&
+                                    listener.expiration <= systemClock.elapsedRealtime()
+                            ) {
+                                // We dozed too long - timeout now, and cancel the pending one
+                                listener.expireMediaTimeout(key, "timeout happened while dozing")
+                                listener.doTimeout()
+                            }
                         }
                     }
                 }
             }
-        })
+        )
     }
 
     override fun onMediaDataLoaded(
@@ -145,10 +153,8 @@
         return mediaListeners[key]?.timedOut ?: false
     }
 
-    private inner class PlaybackStateListener(
-        var key: String,
-        data: MediaData
-    ) : MediaController.Callback() {
+    private inner class PlaybackStateListener(var key: String, data: MediaData) :
+        MediaController.Callback() {
 
         var timedOut = false
         var lastState: PlaybackState? = null
@@ -162,11 +168,12 @@
                 mediaController?.unregisterCallback(this)
                 field = value
                 val token = field.token
-                mediaController = if (token != null) {
-                    mediaControllerFactory.create(token)
-                } else {
-                    null
-                }
+                mediaController =
+                    if (token != null) {
+                        mediaControllerFactory.create(token)
+                    } else {
+                        null
+                    }
                 mediaController?.registerCallback(this)
                 // Let's register the cancellations, but not dispatch events now.
                 // Timeouts didn't happen yet and reentrant events are troublesome.
@@ -212,7 +219,8 @@
             logger.logPlaybackState(key, state)
 
             val playingStateSame = (state?.state?.isPlaying() == isPlaying())
-            val actionsSame = (lastState?.actions == state?.actions) &&
+            val actionsSame =
+                (lastState?.actions == state?.actions) &&
                     areCustomActionListsEqual(lastState?.customActions, state?.customActions)
             val resumptionChanged = resumption != mediaData.resumption
 
@@ -237,15 +245,14 @@
                     return
                 }
                 expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption")
-                val timeout = if (mediaData.resumption) {
-                    RESUME_MEDIA_TIMEOUT
-                } else {
-                    PAUSED_MEDIA_TIMEOUT
-                }
+                val timeout =
+                    if (mediaData.resumption) {
+                        RESUME_MEDIA_TIMEOUT
+                    } else {
+                        PAUSED_MEDIA_TIMEOUT
+                    }
                 expiration = systemClock.elapsedRealtime() + timeout
-                cancellation = mainExecutor.executeDelayed({
-                    doTimeout()
-                }, timeout)
+                cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
             } else {
                 expireMediaTimeout(key, "playback started - $state, $key")
                 timedOut = false
@@ -301,9 +308,11 @@
         firstAction: PlaybackState.CustomAction,
         secondAction: PlaybackState.CustomAction
     ): Boolean {
-        if (firstAction.action != secondAction.action ||
+        if (
+            firstAction.action != secondAction.action ||
                 firstAction.name != secondAction.name ||
-                firstAction.icon != secondAction.icon) {
+                firstAction.icon != secondAction.icon
+        ) {
             return false
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
new file mode 100644
index 0000000..8f3f054
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutLogger.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
+
+import android.media.session.PlaybackState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaTimeoutListenerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+private const val TAG = "MediaTimeout"
+
+/** A buffered log for [MediaTimeoutListener] events */
+@SysUISingleton
+class MediaTimeoutLogger
+@Inject
+constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) {
+    fun logReuseListener(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "reuse listener: $str1" })
+
+    fun logMigrateListener(oldKey: String?, newKey: String?, hadListener: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = oldKey
+                str2 = newKey
+                bool1 = hadListener
+            },
+            { "migrate from $str1 to $str2, had listener? $bool1" }
+        )
+
+    fun logUpdateListener(key: String, wasPlaying: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = wasPlaying
+            },
+            { "updating $str1, was playing? $bool1" }
+        )
+
+    fun logDelayedUpdate(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = key },
+            { "deliver delayed playback state for $str1" }
+        )
+
+    fun logSessionDestroyed(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "session destroyed $str1" })
+
+    fun logPlaybackState(key: String, state: PlaybackState?) =
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = key
+                str2 = state?.toString()
+            },
+            { "state update: key=$str1 state=$str2" }
+        )
+
+    fun logStateCallback(key: String) =
+        buffer.log(TAG, LogLevel.VERBOSE, { str1 = key }, { "dispatching state update for $key" })
+
+    fun logScheduleTimeout(key: String, playing: Boolean, resumption: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = playing
+                bool2 = resumption
+            },
+            { "schedule timeout $str1, playing=$bool1 resumption=$bool2" }
+        )
+
+    fun logCancelIgnored(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "cancellation already exists for $str1" })
+
+    fun logTimeout(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "execute timeout for $str1" })
+
+    fun logTimeoutCancelled(key: String, reason: String) =
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = key
+                str2 = reason
+            },
+            { "media timeout cancelled for $str1, reason: $str2" }
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java
index aca033e..00620b5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaBrowserFactory.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.resume;
 
 import android.content.ComponentName;
 import android.content.Context;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index cc06b6c..4891297 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.resume
 
 import android.content.BroadcastReceiver
 import android.content.ComponentName
@@ -33,6 +33,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.Utils
 import com.android.systemui.util.time.SystemClock
@@ -47,7 +50,9 @@
 private const val MEDIA_PREFERENCE_KEY = "browser_components_"
 
 @SysUISingleton
-class MediaResumeListener @Inject constructor(
+class MediaResumeListener
+@Inject
+constructor(
     private val context: Context,
     private val broadcastDispatcher: BroadcastDispatcher,
     @Background private val backgroundExecutor: Executor,
@@ -59,7 +64,7 @@
 
     private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
     private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> =
-            ConcurrentLinkedQueue()
+        ConcurrentLinkedQueue()
 
     private lateinit var mediaDataManager: MediaDataManager
 
@@ -72,40 +77,49 @@
     private var currentUserId: Int = context.userId
 
     @VisibleForTesting
-    val userChangeReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            if (Intent.ACTION_USER_UNLOCKED == intent.action) {
-                loadMediaResumptionControls()
-            } else if (Intent.ACTION_USER_SWITCHED == intent.action) {
-                currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
-                loadSavedComponents()
+    val userChangeReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                if (Intent.ACTION_USER_UNLOCKED == intent.action) {
+                    loadMediaResumptionControls()
+                } else if (Intent.ACTION_USER_SWITCHED == intent.action) {
+                    currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+                    loadSavedComponents()
+                }
             }
         }
-    }
 
-    private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
-        override fun addTrack(
-            desc: MediaDescription,
-            component: ComponentName,
-            browser: ResumeMediaBrowser
-        ) {
-            val token = browser.token
-            val appIntent = browser.appIntent
-            val pm = context.getPackageManager()
-            var appName: CharSequence = component.packageName
-            val resumeAction = getResumeAction(component)
-            try {
-                appName = pm.getApplicationLabel(
-                        pm.getApplicationInfo(component.packageName, 0))
-            } catch (e: PackageManager.NameNotFoundException) {
-                Log.e(TAG, "Error getting package information", e)
+    private val mediaBrowserCallback =
+        object : ResumeMediaBrowser.Callback() {
+            override fun addTrack(
+                desc: MediaDescription,
+                component: ComponentName,
+                browser: ResumeMediaBrowser
+            ) {
+                val token = browser.token
+                val appIntent = browser.appIntent
+                val pm = context.getPackageManager()
+                var appName: CharSequence = component.packageName
+                val resumeAction = getResumeAction(component)
+                try {
+                    appName =
+                        pm.getApplicationLabel(pm.getApplicationInfo(component.packageName, 0))
+                } catch (e: PackageManager.NameNotFoundException) {
+                    Log.e(TAG, "Error getting package information", e)
+                }
+
+                Log.d(TAG, "Adding resume controls $desc")
+                mediaDataManager.addResumptionControls(
+                    currentUserId,
+                    desc,
+                    resumeAction,
+                    token,
+                    appName.toString(),
+                    appIntent,
+                    component.packageName
+                )
             }
-
-            Log.d(TAG, "Adding resume controls $desc")
-            mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token,
-                appName.toString(), appIntent, component.packageName)
         }
-    }
 
     init {
         if (useMediaResumption) {
@@ -113,8 +127,12 @@
             val unlockFilter = IntentFilter()
             unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
             unlockFilter.addAction(Intent.ACTION_USER_SWITCHED)
-            broadcastDispatcher.registerReceiver(userChangeReceiver, unlockFilter, null,
-                UserHandle.ALL)
+            broadcastDispatcher.registerReceiver(
+                userChangeReceiver,
+                unlockFilter,
+                null,
+                UserHandle.ALL
+            )
             loadSavedComponents()
         }
     }
@@ -123,12 +141,15 @@
         mediaDataManager = manager
 
         // Add listener for resumption setting changes
-        tunerService.addTunable(object : TunerService.Tunable {
-            override fun onTuningChanged(key: String?, newValue: String?) {
-                useMediaResumption = Utils.useMediaResumption(context)
-                mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
-            }
-        }, Settings.Secure.MEDIA_CONTROLS_RESUME)
+        tunerService.addTunable(
+            object : TunerService.Tunable {
+                override fun onTuningChanged(key: String?, newValue: String?) {
+                    useMediaResumption = Utils.useMediaResumption(context)
+                    mediaDataManager.setMediaResumptionEnabled(useMediaResumption)
+                }
+            },
+            Settings.Secure.MEDIA_CONTROLS_RESUME
+        )
     }
 
     private fun loadSavedComponents() {
@@ -136,8 +157,10 @@
         resumeComponents.clear()
         val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
         val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null)
-        val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
-            ?.dropLastWhile { it.isEmpty() }
+        val components =
+            listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())?.dropLastWhile {
+                it.isEmpty()
+            }
         var needsUpdate = false
         components?.forEach {
             val info = it.split("/")
@@ -145,17 +168,18 @@
             val className = info[1]
             val component = ComponentName(packageName, className)
 
-            val lastPlayed = if (info.size == 3) {
-                try {
-                    info[2].toLong()
-                } catch (e: NumberFormatException) {
+            val lastPlayed =
+                if (info.size == 3) {
+                    try {
+                        info[2].toLong()
+                    } catch (e: NumberFormatException) {
+                        needsUpdate = true
+                        systemClock.currentTimeMillis()
+                    }
+                } else {
                     needsUpdate = true
                     systemClock.currentTimeMillis()
                 }
-            } else {
-                needsUpdate = true
-                systemClock.currentTimeMillis()
-            }
             resumeComponents.add(component to lastPlayed)
         }
         Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
@@ -166,9 +190,7 @@
         }
     }
 
-    /**
-     * Load controls for resuming media, if available
-     */
+    /** Load controls for resuming media, if available */
     private fun loadMediaResumptionControls() {
         if (!useMediaResumption) {
             return
@@ -204,9 +226,7 @@
                 val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
                 val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
 
-                val inf = resumeInfo?.filter {
-                    it.serviceInfo.packageName == data.packageName
-                }
+                val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
                 if (inf != null && inf.size > 0) {
                     backgroundExecutor.execute {
                         tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
@@ -227,7 +247,8 @@
         Log.d(TAG, "Testing if we can connect to $componentName")
         // Set null action to prevent additional attempts to connect
         mediaDataManager.setResumeAction(key, null)
-        mediaBrowser = mediaBrowserFactory.create(
+        mediaBrowser =
+            mediaBrowserFactory.create(
                 object : ResumeMediaBrowser.Callback() {
                     override fun onConnected() {
                         Log.d(TAG, "Connected to $componentName")
@@ -250,7 +271,8 @@
                         mediaBrowser = null
                     }
                 },
-                componentName)
+                componentName
+            )
         mediaBrowser?.testConnection()
     }
 
@@ -285,9 +307,7 @@
         prefs.edit().putString(MEDIA_PREFERENCE_KEY + currentUserId, sb.toString()).apply()
     }
 
-    /**
-     * Get a runnable which will resume media playback
-     */
+    /** Get a runnable which will resume media playback */
     private fun getResumeAction(componentName: ComponentName): Runnable {
         return Runnable {
             mediaBrowser = mediaBrowserFactory.create(null, componentName)
@@ -296,8 +316,6 @@
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        pw.apply {
-            println("resumeComponents: $resumeComponents")
-        }
+        pw.apply { println("resumeComponents: $resumeComponents") }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
similarity index 99%
rename from packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
index 40a5653..3493b24 100644
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowser.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.resume;
 
 import android.annotation.Nullable;
 import android.app.PendingIntent;
@@ -293,7 +293,7 @@
     public PendingIntent getAppIntent() {
         PackageManager pm = mContext.getPackageManager();
         Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName());
-        return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        return PendingIntent.getActivity(mContext, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
index 3d1380b..c558227 100644
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserFactory.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.resume;
 
 import android.content.ComponentName;
 import android.content.Context;
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
new file mode 100644
index 0000000..335ce1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserLogger.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.resume
+
+import android.content.ComponentName
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaBrowserLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+/** A logger for events in [ResumeMediaBrowser]. */
+@SysUISingleton
+class ResumeMediaBrowserLogger @Inject constructor(@MediaBrowserLog private val buffer: LogBuffer) {
+    /** Logs that we've initiated a connection to a [android.media.browse.MediaBrowser]. */
+    fun logConnection(componentName: ComponentName, reason: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = componentName.toShortString()
+                str2 = reason
+            },
+            { "Connecting browser for component $str1 due to $str2" }
+        )
+
+    /** Logs that we've disconnected from a [android.media.browse.MediaBrowser]. */
+    fun logDisconnect(componentName: ComponentName) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = componentName.toShortString() },
+            { "Disconnecting browser for component $str1" }
+        )
+
+    /**
+     * Logs that we received a [android.media.session.MediaController.Callback.onSessionDestroyed]
+     * event.
+     *
+     * @param isBrowserConnected true if there's a currently connected
+     * ```
+     *     [android.media.browse.MediaBrowser] and false otherwise.
+     * @param componentName
+     * ```
+     * the component name for the [ResumeMediaBrowser] that triggered this log.
+     */
+    fun logSessionDestroyed(isBrowserConnected: Boolean, componentName: ComponentName) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = isBrowserConnected
+                str1 = componentName.toShortString()
+            },
+            { "Session destroyed. Active browser = $bool1. Browser component = $str1." }
+        )
+}
+
+private const val TAG = "MediaBrowser"
diff --git a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt
index 013683e..d2793bc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/AnimationBindHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/AnimationBindHandler.kt
@@ -14,19 +14,23 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.graphics.drawable.Animatable2
 import android.graphics.drawable.Drawable
 
 /**
- * AnimationBindHandler is responsible for tracking the bound animation state and preventing
- * jank and conflicts due to media notifications arriving at any time during an animation. It
- * does this in two parts.
- *  - Exit animations fired as a result of user input are tracked. When these are running, any
+ * AnimationBindHandler is responsible for tracking the bound animation state and preventing jank
+ * and conflicts due to media notifications arriving at any time during an animation. It does this
+ * in two parts.
+ * - Exit animations fired as a result of user input are tracked. When these are running, any
+ * ```
  *      bind actions are delayed until the animation completes (and then fired in sequence).
- *  - Continuous animations are tracked using their rebind id. Later calls using the same
+ * ```
+ * - Continuous animations are tracked using their rebind id. Later calls using the same
+ * ```
  *      rebind id will be totally ignored to prevent the continuous animation from restarting.
+ * ```
  */
 internal class AnimationBindHandler : Animatable2.AnimationCallback() {
     private val onAnimationsComplete = mutableListOf<() -> Unit>()
@@ -37,10 +41,10 @@
         get() = registrations.any { it.isRunning }
 
     /**
-     * This check prevents rebinding to the action button if the identifier has not changed. A
-     * null value is always considered to be changed. This is used to prevent the connecting
-     * animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by
-     * an application in a row.
+     * This check prevents rebinding to the action button if the identifier has not changed. A null
+     * value is always considered to be changed. This is used to prevent the connecting animation
+     * from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by an
+     * application in a row.
      */
     fun updateRebindId(newRebindId: Int?): Boolean {
         if (rebindId == null || newRebindId == null || rebindId != newRebindId) {
@@ -78,4 +82,4 @@
             onAnimationsComplete.clear()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
new file mode 100644
index 0000000..61ef2f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.content.Context
+import android.content.res.ColorStateList
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.graphics.drawable.RippleDrawable
+import com.android.internal.R
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settingslib.Utils
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.monet.ColorScheme
+
+/**
+ * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme]
+ * is triggered.
+ */
+interface ColorTransition {
+    fun updateColorScheme(scheme: ColorScheme?): Boolean
+}
+
+/**
+ * A [ColorTransition] that animates between two specific colors. It uses a ValueAnimator to execute
+ * the animation and interpolate between the source color and the target color.
+ *
+ * Selection of the target color from the scheme, and application of the interpolated color are
+ * delegated to callbacks.
+ */
+open class AnimatingColorTransition(
+    private val defaultColor: Int,
+    private val extractColor: (ColorScheme) -> Int,
+    private val applyColor: (Int) -> Unit
+) : AnimatorUpdateListener, ColorTransition {
+
+    private val argbEvaluator = ArgbEvaluator()
+    private val valueAnimator = buildAnimator()
+    var sourceColor: Int = defaultColor
+    var currentColor: Int = defaultColor
+    var targetColor: Int = defaultColor
+
+    override fun onAnimationUpdate(animation: ValueAnimator) {
+        currentColor =
+            argbEvaluator.evaluate(animation.animatedFraction, sourceColor, targetColor) as Int
+        applyColor(currentColor)
+    }
+
+    override fun updateColorScheme(scheme: ColorScheme?): Boolean {
+        val newTargetColor = if (scheme == null) defaultColor else extractColor(scheme)
+        if (newTargetColor != targetColor) {
+            sourceColor = currentColor
+            targetColor = newTargetColor
+            valueAnimator.cancel()
+            valueAnimator.start()
+            return true
+        }
+        return false
+    }
+
+    init {
+        applyColor(defaultColor)
+    }
+
+    @VisibleForTesting
+    open fun buildAnimator(): ValueAnimator {
+        val animator = ValueAnimator.ofFloat(0f, 1f)
+        animator.duration = 333
+        animator.addUpdateListener(this)
+        return animator
+    }
+}
+
+typealias AnimatingColorTransitionFactory =
+    (Int, (ColorScheme) -> Int, (Int) -> Unit) -> AnimatingColorTransition
+
+/**
+ * ColorSchemeTransition constructs a ColorTransition for each color in the scheme that needs to be
+ * transitioned when changed. It also sets up the assignment functions for sending the sending the
+ * interpolated colors to the appropriate views.
+ */
+class ColorSchemeTransition
+internal constructor(
+    private val context: Context,
+    private val mediaViewHolder: MediaViewHolder,
+    animatingColorTransitionFactory: AnimatingColorTransitionFactory
+) {
+    constructor(
+        context: Context,
+        mediaViewHolder: MediaViewHolder
+    ) : this(context, mediaViewHolder, ::AnimatingColorTransition)
+
+    val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
+    val surfaceColor =
+        animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor ->
+            val colorList = ColorStateList.valueOf(surfaceColor)
+            mediaViewHolder.seamlessIcon.imageTintList = colorList
+            mediaViewHolder.seamlessText.setTextColor(surfaceColor)
+            mediaViewHolder.albumView.backgroundTintList = colorList
+            mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor)
+        }
+
+    val accentPrimary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            ::accentPrimaryFromScheme
+        ) { accentPrimary ->
+            val accentColorList = ColorStateList.valueOf(accentPrimary)
+            mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
+            mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
+        }
+
+    val accentSecondary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            ::accentSecondaryFromScheme
+        ) { accentSecondary ->
+            val colorList = ColorStateList.valueOf(accentSecondary)
+            (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let {
+                it.setColor(colorList)
+                it.effectColor = colorList
+            }
+        }
+
+    val colorSeamless =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            { colorScheme: ColorScheme ->
+                // A1-100 dark in dark theme, A1-200 in light theme
+                if (
+                    context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+                        UI_MODE_NIGHT_YES
+                )
+                    colorScheme.accent1[2]
+                else colorScheme.accent1[3]
+            },
+            { seamlessColor: Int ->
+                val accentColorList = ColorStateList.valueOf(seamlessColor)
+                mediaViewHolder.seamlessButton.backgroundTintList = accentColorList
+            }
+        )
+
+    val textPrimary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimary),
+            ::textPrimaryFromScheme
+        ) { textPrimary ->
+            mediaViewHolder.titleText.setTextColor(textPrimary)
+            val textColorList = ColorStateList.valueOf(textPrimary)
+            mediaViewHolder.seekBar.thumb.setTintList(textColorList)
+            mediaViewHolder.seekBar.progressTintList = textColorList
+            mediaViewHolder.scrubbingElapsedTimeView.setTextColor(textColorList)
+            mediaViewHolder.scrubbingTotalTimeView.setTextColor(textColorList)
+            for (button in mediaViewHolder.getTransparentActionButtons()) {
+                button.imageTintList = textColorList
+            }
+            mediaViewHolder.gutsViewHolder.setTextPrimaryColor(textPrimary)
+        }
+
+    val textPrimaryInverse =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorPrimaryInverse),
+            ::textPrimaryInverseFromScheme
+        ) { textPrimaryInverse ->
+            mediaViewHolder.actionPlayPause.imageTintList =
+                ColorStateList.valueOf(textPrimaryInverse)
+        }
+
+    val textSecondary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorSecondary),
+            ::textSecondaryFromScheme
+        ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) }
+
+    val textTertiary =
+        animatingColorTransitionFactory(
+            loadDefaultColor(R.attr.textColorTertiary),
+            ::textTertiaryFromScheme
+        ) { textTertiary ->
+            mediaViewHolder.seekBar.progressBackgroundTintList =
+                ColorStateList.valueOf(textTertiary)
+        }
+
+    val colorTransitions =
+        arrayOf(
+            surfaceColor,
+            colorSeamless,
+            accentPrimary,
+            accentSecondary,
+            textPrimary,
+            textPrimaryInverse,
+            textSecondary,
+            textTertiary,
+        )
+
+    private fun loadDefaultColor(id: Int): Int {
+        return Utils.getColorAttr(context, id).defaultColor
+    }
+
+    fun updateColorScheme(colorScheme: ColorScheme?): Boolean {
+        var anyChanged = false
+        colorTransitions.forEach { anyChanged = it.updateColorScheme(colorScheme) || anyChanged }
+        colorScheme?.let { mediaViewHolder.gutsViewHolder.colorScheme = colorScheme }
+        return anyChanged
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
index 121ddd4..9f86cd8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/IlluminationDrawable.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
@@ -42,22 +42,20 @@
 
 private const val BACKGROUND_ANIM_DURATION = 370L
 
-/**
- * Drawable that can draw an animated gradient when tapped.
- */
+/** Drawable that can draw an animated gradient when tapped. */
 @Keep
 class IlluminationDrawable : Drawable() {
 
     private var themeAttrs: IntArray? = null
     private var cornerRadiusOverride = -1f
     var cornerRadius = 0f
-    get() {
-        return if (cornerRadiusOverride >= 0) {
-            cornerRadiusOverride
-        } else {
-            field
+        get() {
+            return if (cornerRadiusOverride >= 0) {
+                cornerRadiusOverride
+            } else {
+                field
+            }
         }
-    }
     private var highlightColor = Color.TRANSPARENT
     private var tmpHsl = floatArrayOf(0f, 0f, 0f)
     private var paint = Paint()
@@ -65,22 +63,27 @@
     private val lightSources = arrayListOf<LightSourceDrawable>()
 
     private var backgroundColor = Color.TRANSPARENT
-    set(value) {
-        if (value == field) {
-            return
+        set(value) {
+            if (value == field) {
+                return
+            }
+            field = value
+            animateBackground()
         }
-        field = value
-        animateBackground()
-    }
 
     private var backgroundAnimation: ValueAnimator? = null
 
-    /**
-     * Draw background and gradient.
-     */
+    /** Draw background and gradient. */
     override fun draw(canvas: Canvas) {
-        canvas.drawRoundRect(0f, 0f, bounds.width().toFloat(), bounds.height().toFloat(),
-                cornerRadius, cornerRadius, paint)
+        canvas.drawRoundRect(
+            0f,
+            0f,
+            bounds.width().toFloat(),
+            bounds.height().toFloat(),
+            cornerRadius,
+            cornerRadius,
+            paint
+        )
     }
 
     override fun getOutline(outline: Outline) {
@@ -105,12 +108,11 @@
 
     private fun updateStateFromTypedArray(a: TypedArray) {
         if (a.hasValue(R.styleable.IlluminationDrawable_cornerRadius)) {
-            cornerRadius = a.getDimension(R.styleable.IlluminationDrawable_cornerRadius,
-                    cornerRadius)
+            cornerRadius =
+                a.getDimension(R.styleable.IlluminationDrawable_cornerRadius, cornerRadius)
         }
         if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
-            highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) /
-                    100f
+            highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
         }
     }
 
@@ -163,34 +165,42 @@
     private fun animateBackground() {
         ColorUtils.colorToHSL(backgroundColor, tmpHsl)
         val L = tmpHsl[2]
-        tmpHsl[2] = MathUtils.constrain(if (L < 1f - highlight) {
-            L + highlight
-        } else {
-            L - highlight
-        }, 0f, 1f)
+        tmpHsl[2] =
+            MathUtils.constrain(
+                if (L < 1f - highlight) {
+                    L + highlight
+                } else {
+                    L - highlight
+                },
+                0f,
+                1f
+            )
 
         val initialBackground = paint.color
         val initialHighlight = highlightColor
         val finalHighlight = ColorUtils.HSLToColor(tmpHsl)
 
         backgroundAnimation?.cancel()
-        backgroundAnimation = ValueAnimator.ofFloat(0f, 1f).apply {
-            duration = BACKGROUND_ANIM_DURATION
-            interpolator = Interpolators.FAST_OUT_LINEAR_IN
-            addUpdateListener {
-                val progress = it.animatedValue as Float
-                paint.color = blendARGB(initialBackground, backgroundColor, progress)
-                highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
-                lightSources.forEach { it.highlightColor = highlightColor }
-                invalidateSelf()
-            }
-            addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
-                    backgroundAnimation = null
+        backgroundAnimation =
+            ValueAnimator.ofFloat(0f, 1f).apply {
+                duration = BACKGROUND_ANIM_DURATION
+                interpolator = Interpolators.FAST_OUT_LINEAR_IN
+                addUpdateListener {
+                    val progress = it.animatedValue as Float
+                    paint.color = blendARGB(initialBackground, backgroundColor, progress)
+                    highlightColor = blendARGB(initialHighlight, finalHighlight, progress)
+                    lightSources.forEach { it.highlightColor = highlightColor }
+                    invalidateSelf()
                 }
-            })
-            start()
-        }
+                addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator?) {
+                            backgroundAnimation = null
+                        }
+                    }
+                )
+                start()
+            }
     }
 
     override fun setTintList(tint: ColorStateList?) {
@@ -215,4 +225,4 @@
     fun setCornerRadiusOverride(cornerRadius: Float?) {
         cornerRadiusOverride = cornerRadius ?: -1f
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
similarity index 79%
rename from packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
index 32600fb..899148b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/KeyguardMediaController.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.content.Context
 import android.content.res.Configuration
@@ -45,7 +45,9 @@
  * switches media player positioning between split pane container vs single pane container
  */
 @SysUISingleton
-class KeyguardMediaController @Inject constructor(
+class KeyguardMediaController
+@Inject
+constructor(
     @param:Named(KEYGUARD) private val mediaHost: MediaHost,
     private val bypassController: KeyguardBypassController,
     private val statusBarStateController: SysuiStatusBarStateController,
@@ -56,34 +58,40 @@
 ) {
 
     init {
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onStateChanged(newState: Int) {
-                refreshMediaPosition()
-            }
-        })
-        configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                updateResources()
-            }
-        })
-
-        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
-            override fun onChange(selfChange: Boolean, uri: Uri?) {
-                if (uri == lockScreenMediaPlayerUri) {
-                    allowMediaPlayerOnLockScreen =
-                            secureSettings.getBoolForUser(
-                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                                    true,
-                                    UserHandle.USER_CURRENT
-                            )
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onStateChanged(newState: Int) {
                     refreshMediaPosition()
                 }
             }
-        }
+        )
+        configurationController.addCallback(
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration?) {
+                    updateResources()
+                }
+            }
+        )
+
+        val settingsObserver: ContentObserver =
+            object : ContentObserver(handler) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    if (uri == lockScreenMediaPlayerUri) {
+                        allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                true,
+                                UserHandle.USER_CURRENT
+                            )
+                        refreshMediaPosition()
+                    }
+                }
+            }
         secureSettings.registerContentObserverForUser(
-                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
-                settingsObserver,
-                UserHandle.USER_ALL)
+            Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+            settingsObserver,
+            UserHandle.USER_ALL
+        )
 
         // First let's set the desired state that we want for this host
         mediaHost.expansion = MediaHostState.EXPANDED
@@ -110,27 +118,21 @@
             refreshMediaPosition()
         }
 
-    /**
-     * Is the media player visible?
-     */
+    /** Is the media player visible? */
     var visible = false
         private set
 
     var visibilityChangedListener: ((Boolean) -> Unit)? = null
 
-    /**
-     * single pane media container placed at the top of the notifications list
-     */
+    /** single pane media container placed at the top of the notifications list */
     var singlePaneContainer: MediaContainerView? = null
         private set
     private var splitShadeContainer: ViewGroup? = null
 
-    /**
-     * Track the media player setting status on lock screen.
-     */
+    /** Track the media player setting status on lock screen. */
     private var allowMediaPlayerOnLockScreen: Boolean = true
     private val lockScreenMediaPlayerUri =
-            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+        secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
 
     /**
      * Attaches media container in single pane mode, situated at the top of the notifications list
@@ -146,9 +148,7 @@
         onMediaHostVisibilityChanged(mediaHost.visible)
     }
 
-    /**
-     * Called whenever the media hosts visibility changes
-     */
+    /** Called whenever the media hosts visibility changes */
     private fun onMediaHostVisibilityChanged(visible: Boolean) {
         refreshMediaPosition()
         if (visible) {
@@ -159,9 +159,7 @@
         }
     }
 
-    /**
-     * Attaches media container in split shade mode, situated to the left of notifications
-     */
+    /** Attaches media container in split shade mode, situated to the left of notifications */
     fun attachSplitShadeContainer(container: ViewGroup) {
         splitShadeContainer = container
         reattachHostView()
@@ -183,9 +181,7 @@
         }
         if (activeContainer?.childCount == 0) {
             // Detach the hostView from its parent view if exists
-            mediaHost.hostView.parent?.let {
-                (it as? ViewGroup)?.removeView(mediaHost.hostView)
-            }
+            mediaHost.hostView.parent?.let { (it as? ViewGroup)?.removeView(mediaHost.hostView) }
             activeContainer.addView(mediaHost.hostView)
         }
     }
@@ -193,7 +189,8 @@
     fun refreshMediaPosition() {
         val keyguardOrUserSwitcher = (statusBarStateController.state == StatusBarState.KEYGUARD)
         // mediaHost.visible required for proper animations handling
-        visible = mediaHost.visible &&
+        visible =
+            mediaHost.visible &&
                 !bypassController.bypassEnabled &&
                 keyguardOrUserSwitcher &&
                 allowMediaPlayerOnLockScreen
diff --git a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
index 711cb36..dd5c2bf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/LightSourceDrawable.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
@@ -55,9 +55,7 @@
     var highlight: Float
 )
 
-/**
- * Drawable that can draw an animated gradient when tapped.
- */
+/** Drawable that can draw an animated gradient when tapped. */
 @Keep
 class LightSourceDrawable : Drawable() {
 
@@ -67,17 +65,15 @@
     private var paint = Paint()
 
     var highlightColor = Color.WHITE
-    set(value) {
-        if (field == value) {
-            return
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            invalidateSelf()
         }
-        field = value
-        invalidateSelf()
-    }
 
-    /**
-     * Draw a small highlight under the finger before expanding (or cancelling) it.
-     */
+    /** Draw a small highlight under the finger before expanding (or cancelling) it. */
     private var active: Boolean = false
         set(value) {
             if (value == field) {
@@ -91,46 +87,54 @@
                 rippleData.progress = RIPPLE_DOWN_PROGRESS
             } else {
                 rippleAnimation?.cancel()
-                rippleAnimation = ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
-                    duration = RIPPLE_CANCEL_DURATION
-                    interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-                    addUpdateListener {
-                        rippleData.alpha = it.animatedValue as Float
-                        invalidateSelf()
-                    }
-                    addListener(object : AnimatorListenerAdapter() {
-                        var cancelled = false
-                        override fun onAnimationCancel(animation: Animator?) {
-                            cancelled = true
-                        }
-
-                        override fun onAnimationEnd(animation: Animator?) {
-                            if (cancelled) {
-                                return
-                            }
-                            rippleData.progress = 0f
-                            rippleData.alpha = 0f
-                            rippleAnimation = null
+                rippleAnimation =
+                    ValueAnimator.ofFloat(rippleData.alpha, 0f).apply {
+                        duration = RIPPLE_CANCEL_DURATION
+                        interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+                        addUpdateListener {
+                            rippleData.alpha = it.animatedValue as Float
                             invalidateSelf()
                         }
-                    })
-                    start()
-                }
+                        addListener(
+                            object : AnimatorListenerAdapter() {
+                                var cancelled = false
+                                override fun onAnimationCancel(animation: Animator?) {
+                                    cancelled = true
+                                }
+
+                                override fun onAnimationEnd(animation: Animator?) {
+                                    if (cancelled) {
+                                        return
+                                    }
+                                    rippleData.progress = 0f
+                                    rippleData.alpha = 0f
+                                    rippleAnimation = null
+                                    invalidateSelf()
+                                }
+                            }
+                        )
+                        start()
+                    }
             }
             invalidateSelf()
         }
 
     private var rippleAnimation: Animator? = null
 
-    /**
-     * Draw background and gradient.
-     */
+    /** Draw background and gradient. */
     override fun draw(canvas: Canvas) {
         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
         val centerColor =
-                ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
-        paint.shader = RadialGradient(rippleData.x, rippleData.y, radius,
-                intArrayOf(centerColor, Color.TRANSPARENT), GRADIENT_STOPS, Shader.TileMode.CLAMP)
+            ColorUtils.setAlphaComponent(highlightColor, (rippleData.alpha * 255).toInt())
+        paint.shader =
+            RadialGradient(
+                rippleData.x,
+                rippleData.y,
+                radius,
+                intArrayOf(centerColor, Color.TRANSPARENT),
+                GRADIENT_STOPS,
+                Shader.TileMode.CLAMP
+            )
         canvas.drawCircle(rippleData.x, rippleData.y, radius, paint)
     }
 
@@ -162,8 +166,8 @@
             rippleData.maxSize = a.getDimension(R.styleable.IlluminationDrawable_rippleMaxSize, 0f)
         }
         if (a.hasValue(R.styleable.IlluminationDrawable_highlight)) {
-            rippleData.highlight = a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) /
-                    100f
+            rippleData.highlight =
+                a.getInteger(R.styleable.IlluminationDrawable_highlight, 0) / 100f
         }
     }
 
@@ -193,40 +197,44 @@
         invalidateSelf()
     }
 
-    /**
-     * Draws an animated ripple that expands fading away.
-     */
+    /** Draws an animated ripple that expands fading away. */
     private fun illuminate() {
         rippleData.alpha = 1f
         invalidateSelf()
 
         rippleAnimation?.cancel()
-        rippleAnimation = AnimatorSet().apply {
-            playTogether(ValueAnimator.ofFloat(1f, 0f).apply {
-                startDelay = 133
-                duration = RIPPLE_ANIM_DURATION - startDelay
-                interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-                addUpdateListener {
-                    rippleData.alpha = it.animatedValue as Float
-                    invalidateSelf()
-                }
-            }, ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
-                duration = RIPPLE_ANIM_DURATION
-                interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-                addUpdateListener {
-                    rippleData.progress = it.animatedValue as Float
-                    invalidateSelf()
-                }
-            })
-            addListener(object : AnimatorListenerAdapter() {
-                override fun onAnimationEnd(animation: Animator?) {
-                    rippleData.progress = 0f
-                    rippleAnimation = null
-                    invalidateSelf()
-                }
-            })
-            start()
-        }
+        rippleAnimation =
+            AnimatorSet().apply {
+                playTogether(
+                    ValueAnimator.ofFloat(1f, 0f).apply {
+                        startDelay = 133
+                        duration = RIPPLE_ANIM_DURATION - startDelay
+                        interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+                        addUpdateListener {
+                            rippleData.alpha = it.animatedValue as Float
+                            invalidateSelf()
+                        }
+                    },
+                    ValueAnimator.ofFloat(rippleData.progress, 1f).apply {
+                        duration = RIPPLE_ANIM_DURATION
+                        interpolator = Interpolators.LINEAR_OUT_SLOW_IN
+                        addUpdateListener {
+                            rippleData.progress = it.animatedValue as Float
+                            invalidateSelf()
+                        }
+                    }
+                )
+                addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator?) {
+                            rippleData.progress = 0f
+                            rippleAnimation = null
+                            invalidateSelf()
+                        }
+                    }
+                )
+                start()
+            }
     }
 
     override fun setHotspot(x: Float, y: Float) {
@@ -251,8 +259,13 @@
 
     override fun getDirtyBounds(): Rect {
         val radius = lerp(rippleData.minSize, rippleData.maxSize, rippleData.progress)
-        val bounds = Rect((rippleData.x - radius).toInt(), (rippleData.y - radius).toInt(),
-                (rippleData.x + radius).toInt(), (rippleData.y + radius).toInt())
+        val bounds =
+            Rect(
+                (rippleData.x - radius).toInt(),
+                (rippleData.y - radius).toInt(),
+                (rippleData.x + radius).toInt(),
+                (rippleData.y + radius).toInt()
+            )
         bounds.union(super.getDirtyBounds())
         return bounds
     }
@@ -293,4 +306,4 @@
 
         return changed
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
new file mode 100644
index 0000000..e38c1ba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -0,0 +1,1327 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.content.res.Configuration
+import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
+import android.util.Log
+import android.util.MathUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.PathInterpolator
+import android.widget.LinearLayout
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.InstanceId
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.ui.MediaControlPanel.SMARTSPACE_CARD_DISMISS_EVENT
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.media.controls.util.SmallHash
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.qs.PageIndicator
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.Utils
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.animation.requiresRemeasuring
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.traceSection
+import java.io.PrintWriter
+import java.util.TreeMap
+import javax.inject.Inject
+import javax.inject.Provider
+
+private const val TAG = "MediaCarouselController"
+private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
+private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+/**
+ * Class that is responsible for keeping the view carousel up to date. This also handles changes in
+ * state and applies them to the media carousel like the expansion.
+ */
+@SysUISingleton
+class MediaCarouselController
+@Inject
+constructor(
+    private val context: Context,
+    private val mediaControlPanelFactory: Provider<MediaControlPanel>,
+    private val visualStabilityProvider: VisualStabilityProvider,
+    private val mediaHostStatesManager: MediaHostStatesManager,
+    private val activityStarter: ActivityStarter,
+    private val systemClock: SystemClock,
+    @Main executor: DelayableExecutor,
+    private val mediaManager: MediaDataManager,
+    configurationController: ConfigurationController,
+    falsingCollector: FalsingCollector,
+    falsingManager: FalsingManager,
+    dumpManager: DumpManager,
+    private val logger: MediaUiEventLogger,
+    private val debugLogger: MediaCarouselControllerLogger
+) : Dumpable {
+    /** The current width of the carousel */
+    private var currentCarouselWidth: Int = 0
+
+    /** The current height of the carousel */
+    private var currentCarouselHeight: Int = 0
+
+    /** Are we currently showing only active players */
+    private var currentlyShowingOnlyActive: Boolean = false
+
+    /** Is the player currently visible (at the end of the transformation */
+    private var playersVisible: Boolean = false
+    /**
+     * The desired location where we'll be at the end of the transformation. Usually this matches
+     * the end location, except when we're still waiting on a state update call.
+     */
+    @MediaLocation private var desiredLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation @VisibleForTesting var currentEndLocation: Int = -1
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation private var currentStartLocation: Int = -1
+
+    /** The progress of the transition or 1.0 if there is no transition happening */
+    private var currentTransitionProgress: Float = 1.0f
+
+    /** The measured width of the carousel */
+    private var carouselMeasureWidth: Int = 0
+
+    /** The measured height of the carousel */
+    private var carouselMeasureHeight: Int = 0
+    private var desiredHostState: MediaHostState? = null
+    private val mediaCarousel: MediaScrollView
+    val mediaCarouselScrollHandler: MediaCarouselScrollHandler
+    val mediaFrame: ViewGroup
+    @VisibleForTesting
+    lateinit var settingsButton: View
+        private set
+    private val mediaContent: ViewGroup
+    @VisibleForTesting val pageIndicator: PageIndicator
+    private val visualStabilityCallback: OnReorderingAllowedListener
+    private var needsReordering: Boolean = false
+    private var keysNeedRemoval = mutableSetOf<String>()
+    var shouldScrollToKey: Boolean = false
+    private var isRtl: Boolean = false
+        set(value) {
+            if (value != field) {
+                field = value
+                mediaFrame.layoutDirection =
+                    if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
+                mediaCarouselScrollHandler.scrollToStart()
+            }
+        }
+    private var currentlyExpanded = true
+        set(value) {
+            if (field != value) {
+                field = value
+                for (player in MediaPlayerData.players()) {
+                    player.setListening(field)
+                }
+            }
+        }
+
+    companion object {
+        const val ANIMATION_BASE_DURATION = 2200f
+        const val DURATION = 167f
+        const val DETAILS_DELAY = 1067f
+        const val CONTROLS_DELAY = 1400f
+        const val PAGINATION_DELAY = 1900f
+        const val MEDIATITLES_DELAY = 1000f
+        const val MEDIACONTAINERS_DELAY = 967f
+        val TRANSFORM_BEZIER = PathInterpolator(0.68F, 0F, 0F, 1F)
+        val REVERSE_BEZIER = PathInterpolator(0F, 0.68F, 1F, 0F)
+
+        fun calculateAlpha(squishinessFraction: Float, delay: Float, duration: Float): Float {
+            val transformStartFraction = delay / ANIMATION_BASE_DURATION
+            val transformDurationFraction = duration / ANIMATION_BASE_DURATION
+            val squishinessToTime = REVERSE_BEZIER.getInterpolation(squishinessFraction)
+            return MathUtils.constrain(
+                (squishinessToTime - transformStartFraction) / transformDurationFraction,
+                0F,
+                1F
+            )
+        }
+    }
+
+    private val configListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onDensityOrFontScaleChanged() {
+                // System font changes should only happen when UMO is offscreen or a flicker may
+                // occur
+                updatePlayers(recreateMedia = true)
+                inflateSettingsButton()
+            }
+
+            override fun onThemeChanged() {
+                updatePlayers(recreateMedia = false)
+                inflateSettingsButton()
+            }
+
+            override fun onConfigChanged(newConfig: Configuration?) {
+                if (newConfig == null) return
+                isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
+            }
+
+            override fun onUiModeChanged() {
+                updatePlayers(recreateMedia = false)
+                inflateSettingsButton()
+            }
+        }
+
+    /**
+     * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
+     * It will be called when the container is out of view.
+     */
+    lateinit var updateUserVisibility: () -> Unit
+    lateinit var updateHostVisibility: () -> Unit
+
+    private val isReorderingAllowed: Boolean
+        get() = visualStabilityProvider.isReorderingAllowed
+
+    init {
+        dumpManager.registerDumpable(TAG, this)
+        mediaFrame = inflateMediaCarousel()
+        mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
+        pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
+        mediaCarouselScrollHandler =
+            MediaCarouselScrollHandler(
+                mediaCarousel,
+                pageIndicator,
+                executor,
+                this::onSwipeToDismiss,
+                this::updatePageIndicatorLocation,
+                this::closeGuts,
+                falsingCollector,
+                falsingManager,
+                this::logSmartspaceImpression,
+                logger
+            )
+        isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
+        inflateSettingsButton()
+        mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
+        configurationController.addCallback(configListener)
+        visualStabilityCallback = OnReorderingAllowedListener {
+            if (needsReordering) {
+                needsReordering = false
+                reorderAllPlayers(previousVisiblePlayerKey = null)
+            }
+
+            keysNeedRemoval.forEach { removePlayer(it) }
+            if (keysNeedRemoval.size > 0) {
+                // Carousel visibility may need to be updated after late removals
+                updateHostVisibility()
+            }
+            keysNeedRemoval.clear()
+
+            // Update user visibility so that no extra impression will be logged when
+            // activeMediaIndex resets to 0
+            if (this::updateUserVisibility.isInitialized) {
+                updateUserVisibility()
+            }
+
+            // Let's reset our scroll position
+            mediaCarouselScrollHandler.scrollToStart()
+        }
+        visualStabilityProvider.addPersistentReorderingAllowedListener(visualStabilityCallback)
+        mediaManager.addListener(
+            object : MediaDataManager.Listener {
+                override fun onMediaDataLoaded(
+                    key: String,
+                    oldKey: String?,
+                    data: MediaData,
+                    immediately: Boolean,
+                    receivedSmartspaceCardLatency: Int,
+                    isSsReactivated: Boolean
+                ) {
+                    debugLogger.logMediaLoaded(key)
+                    if (addOrUpdatePlayer(key, oldKey, data, isSsReactivated)) {
+                        // Log card received if a new resumable media card is added
+                        MediaPlayerData.getMediaPlayer(key)?.let {
+                            /* ktlint-disable max-line-length */
+                            logSmartspaceCardReported(
+                                759, // SMARTSPACE_CARD_RECEIVED
+                                it.mSmartspaceId,
+                                it.mUid,
+                                surfaces =
+                                    intArrayOf(
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                    ),
+                                rank = MediaPlayerData.getMediaPlayerIndex(key)
+                            )
+                            /* ktlint-disable max-line-length */
+                        }
+                        if (
+                            mediaCarouselScrollHandler.visibleToUser &&
+                                mediaCarouselScrollHandler.visibleMediaIndex ==
+                                    MediaPlayerData.getMediaPlayerIndex(key)
+                        ) {
+                            logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
+                        }
+                    } else if (receivedSmartspaceCardLatency != 0) {
+                        // Log resume card received if resumable media card is reactivated and
+                        // resume card is ranked first
+                        MediaPlayerData.players().forEachIndexed { index, it ->
+                            if (it.recommendationViewHolder == null) {
+                                it.mSmartspaceId =
+                                    SmallHash.hash(
+                                        it.mUid + systemClock.currentTimeMillis().toInt()
+                                    )
+                                it.mIsImpressed = false
+                                /* ktlint-disable max-line-length */
+                                logSmartspaceCardReported(
+                                    759, // SMARTSPACE_CARD_RECEIVED
+                                    it.mSmartspaceId,
+                                    it.mUid,
+                                    surfaces =
+                                        intArrayOf(
+                                            SysUiStatsLog
+                                                .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                            SysUiStatsLog
+                                                .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                            SysUiStatsLog
+                                                .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                        ),
+                                    rank = index,
+                                    receivedLatencyMillis = receivedSmartspaceCardLatency
+                                )
+                                /* ktlint-disable max-line-length */
+                            }
+                        }
+                        // If media container area already visible to the user, log impression for
+                        // reactivated card.
+                        if (
+                            mediaCarouselScrollHandler.visibleToUser &&
+                                !mediaCarouselScrollHandler.qsExpanded
+                        ) {
+                            logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
+                        }
+                    }
+
+                    val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
+                    if (canRemove && !Utils.useMediaResumption(context)) {
+                        // This view isn't playing, let's remove this! This happens e.g. when
+                        // dismissing/timing out a view. We still have the data around because
+                        // resumption could be on, but we should save the resources and release
+                        // this.
+                        if (isReorderingAllowed) {
+                            onMediaDataRemoved(key)
+                        } else {
+                            keysNeedRemoval.add(key)
+                        }
+                    } else {
+                        keysNeedRemoval.remove(key)
+                    }
+                }
+
+                override fun onSmartspaceMediaDataLoaded(
+                    key: String,
+                    data: SmartspaceMediaData,
+                    shouldPrioritize: Boolean
+                ) {
+                    debugLogger.logRecommendationLoaded(key)
+                    // Log the case where the hidden media carousel with the existed inactive resume
+                    // media is shown by the Smartspace signal.
+                    if (data.isActive) {
+                        val hasActivatedExistedResumeMedia =
+                            !mediaManager.hasActiveMedia() &&
+                                mediaManager.hasAnyMedia() &&
+                                shouldPrioritize
+                        if (hasActivatedExistedResumeMedia) {
+                            // Log resume card received if resumable media card is reactivated and
+                            // recommendation card is valid and ranked first
+                            MediaPlayerData.players().forEachIndexed { index, it ->
+                                if (it.recommendationViewHolder == null) {
+                                    it.mSmartspaceId =
+                                        SmallHash.hash(
+                                            it.mUid + systemClock.currentTimeMillis().toInt()
+                                        )
+                                    it.mIsImpressed = false
+                                    /* ktlint-disable max-line-length */
+                                    logSmartspaceCardReported(
+                                        759, // SMARTSPACE_CARD_RECEIVED
+                                        it.mSmartspaceId,
+                                        it.mUid,
+                                        surfaces =
+                                            intArrayOf(
+                                                SysUiStatsLog
+                                                    .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                                SysUiStatsLog
+                                                    .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                                SysUiStatsLog
+                                                    .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                            ),
+                                        rank = index,
+                                        receivedLatencyMillis =
+                                            (systemClock.currentTimeMillis() -
+                                                    data.headphoneConnectionTimeMillis)
+                                                .toInt()
+                                    )
+                                    /* ktlint-disable max-line-length */
+                                }
+                            }
+                        }
+                        addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
+                        MediaPlayerData.getMediaPlayer(key)?.let {
+                            /* ktlint-disable max-line-length */
+                            logSmartspaceCardReported(
+                                759, // SMARTSPACE_CARD_RECEIVED
+                                it.mSmartspaceId,
+                                it.mUid,
+                                surfaces =
+                                    intArrayOf(
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                                        SysUiStatsLog
+                                            .SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+                                    ),
+                                rank = MediaPlayerData.getMediaPlayerIndex(key),
+                                receivedLatencyMillis =
+                                    (systemClock.currentTimeMillis() -
+                                            data.headphoneConnectionTimeMillis)
+                                        .toInt()
+                            )
+                            /* ktlint-disable max-line-length */
+                        }
+                        if (
+                            mediaCarouselScrollHandler.visibleToUser &&
+                                mediaCarouselScrollHandler.visibleMediaIndex ==
+                                    MediaPlayerData.getMediaPlayerIndex(key)
+                        ) {
+                            logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
+                        }
+                    } else {
+                        onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
+                    }
+                }
+
+                override fun onMediaDataRemoved(key: String) {
+                    debugLogger.logMediaRemoved(key)
+                    removePlayer(key)
+                }
+
+                override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+                    debugLogger.logRecommendationRemoved(key, immediately)
+                    if (immediately || isReorderingAllowed) {
+                        removePlayer(key)
+                        if (!immediately) {
+                            // Although it wasn't requested, we were able to process the removal
+                            // immediately since reordering is allowed. So, notify hosts to update
+                            if (this@MediaCarouselController::updateHostVisibility.isInitialized) {
+                                updateHostVisibility()
+                            }
+                        }
+                    } else {
+                        keysNeedRemoval.add(key)
+                    }
+                }
+            }
+        )
+        mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+            // The pageIndicator is not laid out yet when we get the current state update,
+            // Lets make sure we have the right dimensions
+            updatePageIndicatorLocation()
+        }
+        mediaHostStatesManager.addCallback(
+            object : MediaHostStatesManager.Callback {
+                override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+                    if (location == desiredLocation) {
+                        onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
+                    }
+                }
+            }
+        )
+    }
+
+    private fun inflateSettingsButton() {
+        val settings =
+            LayoutInflater.from(context)
+                .inflate(R.layout.media_carousel_settings_button, mediaFrame, false) as View
+        if (this::settingsButton.isInitialized) {
+            mediaFrame.removeView(settingsButton)
+        }
+        settingsButton = settings
+        mediaFrame.addView(settingsButton)
+        mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
+        settingsButton.setOnClickListener {
+            logger.logCarouselSettings()
+            activityStarter.startActivity(settingsIntent, true /* dismissShade */)
+        }
+    }
+
+    private fun inflateMediaCarousel(): ViewGroup {
+        val mediaCarousel =
+            LayoutInflater.from(context)
+                .inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup
+        // Because this is inflated when not attached to the true view hierarchy, it resolves some
+        // potential issues to force that the layout direction is defined by the locale
+        // (rather than inherited from the parent, which would resolve to LTR when unattached).
+        mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
+        return mediaCarousel
+    }
+
+    private fun reorderAllPlayers(
+        previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
+        key: String? = null
+    ) {
+        mediaContent.removeAllViews()
+        for (mediaPlayer in MediaPlayerData.players()) {
+            mediaPlayer.mediaViewHolder?.let { mediaContent.addView(it.player) }
+                ?: mediaPlayer.recommendationViewHolder?.let {
+                    mediaContent.addView(it.recommendations)
+                }
+        }
+        mediaCarouselScrollHandler.onPlayersChanged()
+        MediaPlayerData.updateVisibleMediaPlayers()
+        // Automatically scroll to the active player if needed
+        if (shouldScrollToKey) {
+            shouldScrollToKey = false
+            val mediaIndex = key?.let { MediaPlayerData.getMediaPlayerIndex(it) } ?: -1
+            if (mediaIndex != -1) {
+                previousVisiblePlayerKey?.let {
+                    val previousVisibleIndex =
+                        MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
+                    mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
+                }
+                    ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
+            }
+        }
+    }
+
+    // Returns true if new player is added
+    private fun addOrUpdatePlayer(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        isSsReactivated: Boolean
+    ): Boolean =
+        traceSection("MediaCarouselController#addOrUpdatePlayer") {
+            MediaPlayerData.moveIfExists(oldKey, key)
+            val existingPlayer = MediaPlayerData.getMediaPlayer(key)
+            val curVisibleMediaKey =
+                MediaPlayerData.visiblePlayerKeys()
+                    .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+            if (existingPlayer == null) {
+                val newPlayer = mediaControlPanelFactory.get()
+                newPlayer.attachPlayer(
+                    MediaViewHolder.create(LayoutInflater.from(context), mediaContent)
+                )
+                newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
+                val lp =
+                    LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT
+                    )
+                newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
+                newPlayer.bindPlayer(data, key)
+                newPlayer.setListening(currentlyExpanded)
+                MediaPlayerData.addMediaPlayer(
+                    key,
+                    data,
+                    newPlayer,
+                    systemClock,
+                    isSsReactivated,
+                    debugLogger
+                )
+                updatePlayerToState(newPlayer, noAnimation = true)
+                // Media data added from a recommendation card should starts playing.
+                if (
+                    (shouldScrollToKey && data.isPlaying == true) ||
+                        (!shouldScrollToKey && data.active)
+                ) {
+                    reorderAllPlayers(curVisibleMediaKey, key)
+                } else {
+                    needsReordering = true
+                }
+            } else {
+                existingPlayer.bindPlayer(data, key)
+                MediaPlayerData.addMediaPlayer(
+                    key,
+                    data,
+                    existingPlayer,
+                    systemClock,
+                    isSsReactivated,
+                    debugLogger
+                )
+                val packageName = MediaPlayerData.smartspaceMediaData?.packageName ?: String()
+                // In case of recommendations hits.
+                // Check the playing status of media player and the package name.
+                // To make sure we scroll to the right app's media player.
+                if (
+                    isReorderingAllowed ||
+                        shouldScrollToKey &&
+                            data.isPlaying == true &&
+                            packageName == data.packageName
+                ) {
+                    reorderAllPlayers(curVisibleMediaKey, key)
+                } else {
+                    needsReordering = true
+                }
+            }
+            updatePageIndicator()
+            mediaCarouselScrollHandler.onPlayersChanged()
+            mediaFrame.requiresRemeasuring = true
+            // Check postcondition: mediaContent should have the same number of children as there
+            // are
+            // elements in mediaPlayers.
+            if (MediaPlayerData.players().size != mediaContent.childCount) {
+                Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
+            }
+            return existingPlayer == null
+        }
+
+    private fun addSmartspaceMediaRecommendations(
+        key: String,
+        data: SmartspaceMediaData,
+        shouldPrioritize: Boolean
+    ) =
+        traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
+            if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
+            if (MediaPlayerData.getMediaPlayer(key) != null) {
+                Log.w(TAG, "Skip adding smartspace target in carousel")
+                return
+            }
+
+            val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
+            existingSmartspaceMediaKey?.let {
+                val removedPlayer =
+                    MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey, true)
+                removedPlayer?.run {
+                    debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey)
+                }
+            }
+
+            val newRecs = mediaControlPanelFactory.get()
+            newRecs.attachRecommendation(
+                RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
+            )
+            newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
+            val lp =
+                LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT
+                )
+            newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
+            newRecs.bindRecommendation(data)
+            val curVisibleMediaKey =
+                MediaPlayerData.visiblePlayerKeys()
+                    .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+            MediaPlayerData.addMediaRecommendation(
+                key,
+                data,
+                newRecs,
+                shouldPrioritize,
+                systemClock,
+                debugLogger
+            )
+            updatePlayerToState(newRecs, noAnimation = true)
+            reorderAllPlayers(curVisibleMediaKey)
+            updatePageIndicator()
+            mediaFrame.requiresRemeasuring = true
+            // Check postcondition: mediaContent should have the same number of children as there
+            // are
+            // elements in mediaPlayers.
+            if (MediaPlayerData.players().size != mediaContent.childCount) {
+                Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
+            }
+        }
+
+    fun removePlayer(
+        key: String,
+        dismissMediaData: Boolean = true,
+        dismissRecommendation: Boolean = true
+    ) {
+        if (key == MediaPlayerData.smartspaceMediaKey()) {
+            MediaPlayerData.smartspaceMediaData?.let {
+                logger.logRecommendationRemoved(it.packageName, it.instanceId)
+            }
+        }
+        val removed =
+            MediaPlayerData.removeMediaPlayer(key, dismissMediaData || dismissRecommendation)
+        removed?.apply {
+            mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
+            mediaContent.removeView(removed.mediaViewHolder?.player)
+            mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
+            removed.onDestroy()
+            mediaCarouselScrollHandler.onPlayersChanged()
+            updatePageIndicator()
+
+            if (dismissMediaData) {
+                // Inform the media manager of a potentially late dismissal
+                mediaManager.dismissMediaData(key, delay = 0L)
+            }
+            if (dismissRecommendation) {
+                // Inform the media manager of a potentially late dismissal
+                mediaManager.dismissSmartspaceRecommendation(key, delay = 0L)
+            }
+        }
+    }
+
+    private fun updatePlayers(recreateMedia: Boolean) {
+        pageIndicator.tintList =
+            ColorStateList.valueOf(context.getColor(R.color.media_paging_indicator))
+
+        MediaPlayerData.mediaData().forEach { (key, data, isSsMediaRec) ->
+            if (isSsMediaRec) {
+                val smartspaceMediaData = MediaPlayerData.smartspaceMediaData
+                removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
+                smartspaceMediaData?.let {
+                    addSmartspaceMediaRecommendations(
+                        it.targetId,
+                        it,
+                        MediaPlayerData.shouldPrioritizeSs
+                    )
+                }
+            } else {
+                val isSsReactivated = MediaPlayerData.isSsReactivated(key)
+                if (recreateMedia) {
+                    removePlayer(key, dismissMediaData = false, dismissRecommendation = false)
+                }
+                addOrUpdatePlayer(
+                    key = key,
+                    oldKey = null,
+                    data = data,
+                    isSsReactivated = isSsReactivated
+                )
+            }
+        }
+    }
+
+    private fun updatePageIndicator() {
+        val numPages = mediaContent.getChildCount()
+        pageIndicator.setNumPages(numPages)
+        if (numPages == 1) {
+            pageIndicator.setLocation(0f)
+        }
+        updatePageIndicatorAlpha()
+    }
+
+    /**
+     * Set a new interpolated state for all players. This is a state that is usually controlled by a
+     * finger movement where the user drags from one state to the next.
+     *
+     * @param startLocation the start location of our state or -1 if this is directly set
+     * @param endLocation the ending location of our state.
+     * @param progress the progress of the transition between startLocation and endlocation. If
+     * ```
+     *                 this is not a guided transformation, this will be 1.0f
+     * @param immediately
+     * ```
+     * should this state be applied immediately, canceling all animations?
+     */
+    fun setCurrentState(
+        @MediaLocation startLocation: Int,
+        @MediaLocation endLocation: Int,
+        progress: Float,
+        immediately: Boolean
+    ) {
+        if (
+            startLocation != currentStartLocation ||
+                endLocation != currentEndLocation ||
+                progress != currentTransitionProgress ||
+                immediately
+        ) {
+            currentStartLocation = startLocation
+            currentEndLocation = endLocation
+            currentTransitionProgress = progress
+            for (mediaPlayer in MediaPlayerData.players()) {
+                updatePlayerToState(mediaPlayer, immediately)
+            }
+            maybeResetSettingsCog()
+            updatePageIndicatorAlpha()
+        }
+    }
+
+    @VisibleForTesting
+    fun updatePageIndicatorAlpha() {
+        val hostStates = mediaHostStatesManager.mediaHostStates
+        val endIsVisible = hostStates[currentEndLocation]?.visible ?: false
+        val startIsVisible = hostStates[currentStartLocation]?.visible ?: false
+        val startAlpha = if (startIsVisible) 1.0f else 0.0f
+        // when squishing in split shade, only use endState, which keeps changing
+        // to provide squishFraction
+        val squishFraction = hostStates[currentEndLocation]?.squishFraction ?: 1.0F
+        val endAlpha =
+            (if (endIsVisible) 1.0f else 0.0f) *
+                calculateAlpha(squishFraction, PAGINATION_DELAY, DURATION)
+        var alpha = 1.0f
+        if (!endIsVisible || !startIsVisible) {
+            var progress = currentTransitionProgress
+            if (!endIsVisible) {
+                progress = 1.0f - progress
+            }
+            // Let's fade in quickly at the end where the view is visible
+            progress =
+                MathUtils.constrain(MathUtils.map(0.95f, 1.0f, 0.0f, 1.0f, progress), 0.0f, 1.0f)
+            alpha = MathUtils.lerp(startAlpha, endAlpha, progress)
+        }
+        pageIndicator.alpha = alpha
+    }
+
+    private fun updatePageIndicatorLocation() {
+        // Update the location of the page indicator, carousel clipping
+        val translationX =
+            if (isRtl) {
+                (pageIndicator.width - currentCarouselWidth) / 2.0f
+            } else {
+                (currentCarouselWidth - pageIndicator.width) / 2.0f
+            }
+        pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
+        val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
+        pageIndicator.translationY =
+            (currentCarouselHeight - pageIndicator.height - layoutParams.bottomMargin).toFloat()
+    }
+
+    /** Update the dimension of this carousel. */
+    private fun updateCarouselDimensions() {
+        var width = 0
+        var height = 0
+        for (mediaPlayer in MediaPlayerData.players()) {
+            val controller = mediaPlayer.mediaViewController
+            // When transitioning the view to gone, the view gets smaller, but the translation
+            // Doesn't, let's add the translation
+            width = Math.max(width, controller.currentWidth + controller.translationX.toInt())
+            height = Math.max(height, controller.currentHeight + controller.translationY.toInt())
+        }
+        if (width != currentCarouselWidth || height != currentCarouselHeight) {
+            currentCarouselWidth = width
+            currentCarouselHeight = height
+            mediaCarouselScrollHandler.setCarouselBounds(
+                currentCarouselWidth,
+                currentCarouselHeight
+            )
+            updatePageIndicatorLocation()
+            updatePageIndicatorAlpha()
+        }
+    }
+
+    private fun maybeResetSettingsCog() {
+        val hostStates = mediaHostStatesManager.mediaHostStates
+        val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia ?: true
+        val startShowsActive =
+            hostStates[currentStartLocation]?.showsOnlyActiveMedia ?: endShowsActive
+        if (
+            currentlyShowingOnlyActive != endShowsActive ||
+                ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
+                    startShowsActive != endShowsActive)
+        ) {
+            // Whenever we're transitioning from between differing states or the endstate differs
+            // we reset the translation
+            currentlyShowingOnlyActive = endShowsActive
+            mediaCarouselScrollHandler.resetTranslation(animate = true)
+        }
+    }
+
+    private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
+        mediaPlayer.mediaViewController.setCurrentState(
+            startLocation = currentStartLocation,
+            endLocation = currentEndLocation,
+            transitionProgress = currentTransitionProgress,
+            applyImmediately = noAnimation
+        )
+    }
+
+    /**
+     * The desired location of this view has changed. We should remeasure the view to match the new
+     * bounds and kick off bounds animations if necessary. If an animation is happening, an
+     * animation is kicked of externally, which sets a new current state until we reach the
+     * targetState.
+     *
+     * @param desiredLocation the location we're going to
+     * @param desiredHostState the target state we're transitioning to
+     * @param animate should this be animated
+     */
+    fun onDesiredLocationChanged(
+        desiredLocation: Int,
+        desiredHostState: MediaHostState?,
+        animate: Boolean,
+        duration: Long = 200,
+        startDelay: Long = 0
+    ) =
+        traceSection("MediaCarouselController#onDesiredLocationChanged") {
+            desiredHostState?.let {
+                if (this.desiredLocation != desiredLocation) {
+                    // Only log an event when location changes
+                    logger.logCarouselPosition(desiredLocation)
+                }
+
+                // This is a hosting view, let's remeasure our players
+                this.desiredLocation = desiredLocation
+                this.desiredHostState = it
+                currentlyExpanded = it.expansion > 0
+
+                val shouldCloseGuts =
+                    !currentlyExpanded &&
+                        !mediaManager.hasActiveMediaOrRecommendation() &&
+                        desiredHostState.showsOnlyActiveMedia
+
+                for (mediaPlayer in MediaPlayerData.players()) {
+                    if (animate) {
+                        mediaPlayer.mediaViewController.animatePendingStateChange(
+                            duration = duration,
+                            delay = startDelay
+                        )
+                    }
+                    if (shouldCloseGuts && mediaPlayer.mediaViewController.isGutsVisible) {
+                        mediaPlayer.closeGuts(!animate)
+                    }
+
+                    mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
+                }
+                mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
+                mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
+                val nowVisible = it.visible
+                if (nowVisible != playersVisible) {
+                    playersVisible = nowVisible
+                    if (nowVisible) {
+                        mediaCarouselScrollHandler.resetTranslation()
+                    }
+                }
+                updateCarouselSize()
+            }
+        }
+
+    fun closeGuts(immediate: Boolean = true) {
+        MediaPlayerData.players().forEach { it.closeGuts(immediate) }
+    }
+
+    /** Update the size of the carousel, remeasuring it if necessary. */
+    private fun updateCarouselSize() {
+        val width = desiredHostState?.measurementInput?.width ?: 0
+        val height = desiredHostState?.measurementInput?.height ?: 0
+        if (
+            width != carouselMeasureWidth && width != 0 ||
+                height != carouselMeasureHeight && height != 0
+        ) {
+            carouselMeasureWidth = width
+            carouselMeasureHeight = height
+            val playerWidthPlusPadding =
+                carouselMeasureWidth +
+                    context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
+            // Let's remeasure the carousel
+            val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
+            val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
+            mediaCarousel.measure(widthSpec, heightSpec)
+            mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
+            // Update the padding after layout; view widths are used in RTL to calculate scrollX
+            mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
+        }
+    }
+
+    /** Log the user impression for media card at visibleMediaIndex. */
+    fun logSmartspaceImpression(qsExpanded: Boolean) {
+        val visibleMediaIndex = mediaCarouselScrollHandler.visibleMediaIndex
+        if (MediaPlayerData.players().size > visibleMediaIndex) {
+            val mediaControlPanel = MediaPlayerData.getMediaControlPanel(visibleMediaIndex)
+            val hasActiveMediaOrRecommendationCard =
+                MediaPlayerData.hasActiveMediaOrRecommendationCard()
+            if (!hasActiveMediaOrRecommendationCard && !qsExpanded) {
+                // Skip logging if on LS or QQS, and there is no active media card
+                return
+            }
+            mediaControlPanel?.let {
+                logSmartspaceCardReported(
+                    800, // SMARTSPACE_CARD_SEEN
+                    it.mSmartspaceId,
+                    it.mUid,
+                    intArrayOf(it.surfaceForSmartspaceLogging)
+                )
+                it.mIsImpressed = true
+            }
+        }
+    }
+
+    @JvmOverloads
+    /**
+     * Log Smartspace events
+     *
+     * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
+     * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
+     * instanceId
+     * @param uid uid for the application that media comes from
+     * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
+     * the event happened
+     * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
+     * for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
+     * @param interactedSubcardCardinality how many media items were shown to the user when there is
+     * user interaction
+     * @param rank the rank for media card in the media carousel, starting from 0
+     * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
+     * between headphone connection to sysUI displays media recommendation card
+     * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
+     */
+    fun logSmartspaceCardReported(
+        eventId: Int,
+        instanceId: Int,
+        uid: Int,
+        surfaces: IntArray,
+        interactedSubcardRank: Int = 0,
+        interactedSubcardCardinality: Int = 0,
+        rank: Int = mediaCarouselScrollHandler.visibleMediaIndex,
+        receivedLatencyMillis: Int = 0,
+        isSwipeToDismiss: Boolean = false
+    ) {
+        if (MediaPlayerData.players().size <= rank) {
+            return
+        }
+
+        val mediaControlKey = MediaPlayerData.visiblePlayerKeys().elementAt(rank)
+        // Only log media resume card when Smartspace data is available
+        if (
+            !mediaControlKey.isSsMediaRec &&
+                !mediaManager.smartspaceMediaData.isActive &&
+                MediaPlayerData.smartspaceMediaData == null
+        ) {
+            return
+        }
+
+        val cardinality = mediaContent.getChildCount()
+        surfaces.forEach { surface ->
+            /* ktlint-disable max-line-length */
+            SysUiStatsLog.write(
+                SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
+                eventId,
+                instanceId,
+                // Deprecated, replaced with AiAi feature type so we don't need to create logging
+                // card type for each new feature.
+                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
+                surface,
+                // Use -1 as rank value to indicate user swipe to dismiss the card
+                if (isSwipeToDismiss) -1 else rank,
+                cardinality,
+                if (mediaControlKey.isSsMediaRec) 15 // MEDIA_RECOMMENDATION
+                else if (mediaControlKey.isSsReactivated) 43 // MEDIA_RESUME_SS_ACTIVATED
+                else 31, // MEDIA_RESUME
+                uid,
+                interactedSubcardRank,
+                interactedSubcardCardinality,
+                receivedLatencyMillis,
+                null, // Media cards cannot have subcards.
+                null // Media cards don't have dimensions today.
+            )
+            /* ktlint-disable max-line-length */
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "Log Smartspace card event id: $eventId instance id: $instanceId" +
+                        " surface: $surface rank: $rank cardinality: $cardinality " +
+                        "isRecommendationCard: ${mediaControlKey.isSsMediaRec} " +
+                        "isSsReactivated: ${mediaControlKey.isSsReactivated}" +
+                        "uid: $uid " +
+                        "interactedSubcardRank: $interactedSubcardRank " +
+                        "interactedSubcardCardinality: $interactedSubcardCardinality " +
+                        "received_latency_millis: $receivedLatencyMillis"
+                )
+            }
+        }
+    }
+
+    private fun onSwipeToDismiss() {
+        MediaPlayerData.players().forEachIndexed { index, it ->
+            if (it.mIsImpressed) {
+                logSmartspaceCardReported(
+                    SMARTSPACE_CARD_DISMISS_EVENT,
+                    it.mSmartspaceId,
+                    it.mUid,
+                    intArrayOf(it.surfaceForSmartspaceLogging),
+                    rank = index,
+                    isSwipeToDismiss = true
+                )
+                // Reset card impressed state when swipe to dismissed
+                it.mIsImpressed = false
+            }
+        }
+        logger.logSwipeDismiss()
+        mediaManager.onSwipeToDismiss()
+    }
+
+    fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
+        return MediaPlayerData.playerKeys()
+            .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
+            ?.data
+            ?.clickIntent
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("keysNeedRemoval: $keysNeedRemoval")
+            println("dataKeys: ${MediaPlayerData.dataKeys()}")
+            println("orderedPlayerSortKeys: ${MediaPlayerData.playerKeys()}")
+            println("visiblePlayerSortKeys: ${MediaPlayerData.visiblePlayerKeys()}")
+            println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
+            println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
+            println("current size: $currentCarouselWidth x $currentCarouselHeight")
+            println("location: $desiredLocation")
+            println(
+                "state: ${desiredHostState?.expansion}, " +
+                    "only active ${desiredHostState?.showsOnlyActiveMedia}"
+            )
+        }
+    }
+}
+
+@VisibleForTesting
+internal object MediaPlayerData {
+    private val EMPTY =
+        MediaData(
+            userId = -1,
+            initialized = false,
+            app = null,
+            appIcon = null,
+            artist = null,
+            song = null,
+            artwork = null,
+            actions = emptyList(),
+            actionsToShowInCompact = emptyList(),
+            packageName = "INVALID",
+            token = null,
+            clickIntent = null,
+            device = null,
+            active = true,
+            resumeAction = null,
+            instanceId = InstanceId.fakeInstanceId(-1),
+            appUid = -1
+        )
+    // Whether should prioritize Smartspace card.
+    internal var shouldPrioritizeSs: Boolean = false
+        private set
+    internal var smartspaceMediaData: SmartspaceMediaData? = null
+        private set
+
+    data class MediaSortKey(
+        val isSsMediaRec: Boolean, // Whether the item represents a Smartspace media recommendation.
+        val data: MediaData,
+        val key: String,
+        val updateTime: Long = 0,
+        val isSsReactivated: Boolean = false
+    )
+
+    private val comparator =
+        compareByDescending<MediaSortKey> {
+                it.data.isPlaying == true && it.data.playbackLocation == MediaData.PLAYBACK_LOCAL
+            }
+            .thenByDescending {
+                it.data.isPlaying == true &&
+                    it.data.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL
+            }
+            .thenByDescending { it.data.active }
+            .thenByDescending { shouldPrioritizeSs == it.isSsMediaRec }
+            .thenByDescending { !it.data.resumption }
+            .thenByDescending { it.data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE }
+            .thenByDescending { it.data.lastActive }
+            .thenByDescending { it.updateTime }
+            .thenByDescending { it.data.notificationKey }
+
+    private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
+    private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
+    // A map that tracks order of visible media players before they get reordered.
+    private val visibleMediaPlayers = LinkedHashMap<String, MediaSortKey>()
+
+    fun addMediaPlayer(
+        key: String,
+        data: MediaData,
+        player: MediaControlPanel,
+        clock: SystemClock,
+        isSsReactivated: Boolean,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
+        val removedPlayer = removeMediaPlayer(key)
+        if (removedPlayer != null && removedPlayer != player) {
+            debugLogger?.logPotentialMemoryLeak(key)
+        }
+        val sortKey =
+            MediaSortKey(
+                isSsMediaRec = false,
+                data,
+                key,
+                clock.currentTimeMillis(),
+                isSsReactivated = isSsReactivated
+            )
+        mediaData.put(key, sortKey)
+        mediaPlayers.put(sortKey, player)
+        visibleMediaPlayers.put(key, sortKey)
+    }
+
+    fun addMediaRecommendation(
+        key: String,
+        data: SmartspaceMediaData,
+        player: MediaControlPanel,
+        shouldPrioritize: Boolean,
+        clock: SystemClock,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
+        shouldPrioritizeSs = shouldPrioritize
+        val removedPlayer = removeMediaPlayer(key)
+        if (removedPlayer != null && removedPlayer != player) {
+            debugLogger?.logPotentialMemoryLeak(key)
+        }
+        val sortKey =
+            MediaSortKey(
+                isSsMediaRec = true,
+                EMPTY.copy(isPlaying = false),
+                key,
+                clock.currentTimeMillis(),
+                isSsReactivated = true
+            )
+        mediaData.put(key, sortKey)
+        mediaPlayers.put(sortKey, player)
+        visibleMediaPlayers.put(key, sortKey)
+        smartspaceMediaData = data
+    }
+
+    fun moveIfExists(
+        oldKey: String?,
+        newKey: String,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
+        if (oldKey == null || oldKey == newKey) {
+            return
+        }
+
+        mediaData.remove(oldKey)?.let {
+            // MediaPlayer should not be visible
+            // no need to set isDismissed flag.
+            val removedPlayer = removeMediaPlayer(newKey)
+            removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
+            mediaData.put(newKey, it)
+        }
+    }
+
+    fun getMediaControlPanel(visibleIndex: Int): MediaControlPanel? {
+        return mediaPlayers.get(visiblePlayerKeys().elementAt(visibleIndex))
+    }
+
+    fun getMediaPlayer(key: String): MediaControlPanel? {
+        return mediaData.get(key)?.let { mediaPlayers.get(it) }
+    }
+
+    fun getMediaPlayerIndex(key: String): Int {
+        val sortKey = mediaData.get(key)
+        mediaPlayers.entries.forEachIndexed { index, e ->
+            if (e.key == sortKey) {
+                return index
+            }
+        }
+        return -1
+    }
+
+    /**
+     * Removes media player given the key.
+     * @param isDismissed determines whether the media player is removed from the carousel.
+     */
+    fun removeMediaPlayer(key: String, isDismissed: Boolean = false) =
+        mediaData.remove(key)?.let {
+            if (it.isSsMediaRec) {
+                smartspaceMediaData = null
+            }
+            if (isDismissed) {
+                visibleMediaPlayers.remove(key)
+            }
+            mediaPlayers.remove(it)
+        }
+
+    fun mediaData() =
+        mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
+
+    fun dataKeys() = mediaData.keys
+
+    fun players() = mediaPlayers.values
+
+    fun playerKeys() = mediaPlayers.keys
+
+    fun visiblePlayerKeys() = visibleMediaPlayers.values
+
+    /** Returns the index of the first non-timeout media. */
+    fun firstActiveMediaIndex(): Int {
+        mediaPlayers.entries.forEachIndexed { index, e ->
+            if (!e.key.isSsMediaRec && e.key.data.active) {
+                return index
+            }
+        }
+        return -1
+    }
+
+    /** Returns the existing Smartspace target id. */
+    fun smartspaceMediaKey(): String? {
+        mediaData.entries.forEach { e ->
+            if (e.value.isSsMediaRec) {
+                return e.key
+            }
+        }
+        return null
+    }
+
+    @VisibleForTesting
+    fun clear() {
+        mediaData.clear()
+        mediaPlayers.clear()
+        visibleMediaPlayers.clear()
+    }
+
+    /* Returns true if there is active media player card or recommendation card */
+    fun hasActiveMediaOrRecommendationCard(): Boolean {
+        if (smartspaceMediaData != null && smartspaceMediaData?.isActive!!) {
+            return true
+        }
+        if (firstActiveMediaIndex() != -1) {
+            return true
+        }
+        return false
+    }
+
+    fun isSsReactivated(key: String): Boolean = mediaData.get(key)?.isSsReactivated ?: false
+
+    /**
+     * This method is called when media players are reordered. To make sure we have the new version
+     * of the order of media players visible to user.
+     */
+    fun updateVisibleMediaPlayers() {
+        visibleMediaPlayers.clear()
+        playerKeys().forEach { visibleMediaPlayers.put(it.key, it) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
new file mode 100644
index 0000000..eed1bd7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselControllerLogger.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaCarouselControllerLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+/** A debug logger for [MediaCarouselController]. */
+@SysUISingleton
+class MediaCarouselControllerLogger
+@Inject
+constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) {
+    /**
+     * Log that there might be a potential memory leak for the [MediaControlPanel] and/or
+     * [MediaViewController] related to [key].
+     */
+    fun logPotentialMemoryLeak(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = key },
+            {
+                "Potential memory leak: " +
+                    "Removing control panel for $str1 from map without calling #onDestroy"
+            }
+        )
+
+    fun logMediaLoaded(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add player $str1" })
+
+    fun logMediaRemoved(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
+
+    fun logRecommendationLoaded(key: String) =
+        buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
+
+    fun logRecommendationRemoved(key: String, immediately: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = immediately
+            },
+            { "removing recommendation $str1, immediate=$bool1" }
+        )
+}
+
+private const val TAG = "MediaCarouselCtlrLog"
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
similarity index 67%
rename from packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
index a776897..36b2eda 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselScrollHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.graphics.Outline
 import android.util.MathUtils
@@ -31,6 +31,7 @@
 import com.android.systemui.R
 import com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PageIndicator
 import com.android.systemui.util.concurrency.DelayableExecutor
@@ -43,16 +44,13 @@
 private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
 
 /**
- * Default spring configuration to use for animations where stiffness and/or damping ratio
- * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
+ * Default spring configuration to use for animations where stiffness and/or damping ratio were not
+ * provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
  */
-private val translationConfig = PhysicsAnimator.SpringConfig(
-        SpringForce.STIFFNESS_LOW,
-        SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+private val translationConfig =
+    PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY)
 
-/**
- * A controller class for the media scrollview, responsible for touch handling
- */
+/** A controller class for the media scrollview, responsible for touch handling */
 class MediaCarouselScrollHandler(
     private val scrollView: MediaScrollView,
     private val pageIndicator: PageIndicator,
@@ -65,57 +63,36 @@
     private val logSmartspaceImpression: (Boolean) -> Unit,
     private val logger: MediaUiEventLogger
 ) {
-    /**
-     * Is the view in RTL
-     */
-    val isRtl: Boolean get() = scrollView.isLayoutRtl
-    /**
-     * Do we need falsing protection?
-     */
+    /** Is the view in RTL */
+    val isRtl: Boolean
+        get() = scrollView.isLayoutRtl
+    /** Do we need falsing protection? */
     var falsingProtectionNeeded: Boolean = false
-    /**
-     * The width of the carousel
-     */
+    /** The width of the carousel */
     private var carouselWidth: Int = 0
 
-    /**
-     * The height of the carousel
-     */
+    /** The height of the carousel */
     private var carouselHeight: Int = 0
 
-    /**
-     * How much are we scrolled into the current media?
-     */
+    /** How much are we scrolled into the current media? */
     private var cornerRadius: Int = 0
 
-    /**
-     * The content where the players are added
-     */
+    /** The content where the players are added */
     private var mediaContent: ViewGroup
-    /**
-     * The gesture detector to detect touch gestures
-     */
+    /** The gesture detector to detect touch gestures */
     private val gestureDetector: GestureDetectorCompat
 
-    /**
-     * The settings button view
-     */
+    /** The settings button view */
     private lateinit var settingsButton: View
 
-    /**
-     * What's the currently visible player index?
-     */
+    /** What's the currently visible player index? */
     var visibleMediaIndex: Int = 0
         private set
 
-    /**
-     * How much are we scrolled into the current media?
-     */
+    /** How much are we scrolled into the current media? */
     private var scrollIntoCurrentMedia: Int = 0
 
-    /**
-     * how much is the content translated in X
-     */
+    /** how much is the content translated in X */
     var contentTranslation = 0.0f
         private set(value) {
             field = value
@@ -125,9 +102,7 @@
             updateClipToOutline()
         }
 
-    /**
-     * The width of a player including padding
-     */
+    /** The width of a player including padding */
     var playerWidthPlusPadding: Int = 0
         set(value) {
             field = value
@@ -135,82 +110,75 @@
             // it's still at the same place
             var newRelativeScroll = visibleMediaIndex * playerWidthPlusPadding
             if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
-                newRelativeScroll += playerWidthPlusPadding -
-                        (scrollIntoCurrentMedia - playerWidthPlusPadding)
+                newRelativeScroll +=
+                    playerWidthPlusPadding - (scrollIntoCurrentMedia - playerWidthPlusPadding)
             } else {
                 newRelativeScroll += scrollIntoCurrentMedia
             }
             scrollView.relativeScrollX = newRelativeScroll
         }
 
-    /**
-     * Does the dismiss currently show the setting cog?
-     */
+    /** Does the dismiss currently show the setting cog? */
     var showsSettingsButton: Boolean = false
 
-    /**
-     * A utility to detect gestures, used in the touch listener
-     */
-    private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
-        override fun onFling(
-            eStart: MotionEvent?,
-            eCurrent: MotionEvent?,
-            vX: Float,
-            vY: Float
-        ) = onFling(vX, vY)
+    /** A utility to detect gestures, used in the touch listener */
+    private val gestureListener =
+        object : GestureDetector.SimpleOnGestureListener() {
+            override fun onFling(
+                eStart: MotionEvent?,
+                eCurrent: MotionEvent?,
+                vX: Float,
+                vY: Float
+            ) = onFling(vX, vY)
 
-        override fun onScroll(
-            down: MotionEvent?,
-            lastMotion: MotionEvent?,
-            distanceX: Float,
-            distanceY: Float
-        ) = onScroll(down!!, lastMotion!!, distanceX)
+            override fun onScroll(
+                down: MotionEvent?,
+                lastMotion: MotionEvent?,
+                distanceX: Float,
+                distanceY: Float
+            ) = onScroll(down!!, lastMotion!!, distanceX)
 
-        override fun onDown(e: MotionEvent?): Boolean {
-            if (falsingProtectionNeeded) {
-                falsingCollector.onNotificationStartDismissing()
+            override fun onDown(e: MotionEvent?): Boolean {
+                if (falsingProtectionNeeded) {
+                    falsingCollector.onNotificationStartDismissing()
+                }
+                return false
             }
-            return false
         }
-    }
 
-    /**
-     * The touch listener for the scroll view
-     */
-    private val touchListener = object : Gefingerpoken {
-        override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
-        override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
-    }
+    /** The touch listener for the scroll view */
+    private val touchListener =
+        object : Gefingerpoken {
+            override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
+            override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
+        }
 
-    /**
-     * A listener that is invoked when the scrolling changes to update player visibilities
-     */
-    private val scrollChangedListener = object : View.OnScrollChangeListener {
-        override fun onScrollChange(
-            v: View?,
-            scrollX: Int,
-            scrollY: Int,
-            oldScrollX: Int,
-            oldScrollY: Int
-        ) {
-            if (playerWidthPlusPadding == 0) {
-                return
+    /** A listener that is invoked when the scrolling changes to update player visibilities */
+    private val scrollChangedListener =
+        object : View.OnScrollChangeListener {
+            override fun onScrollChange(
+                v: View?,
+                scrollX: Int,
+                scrollY: Int,
+                oldScrollX: Int,
+                oldScrollY: Int
+            ) {
+                if (playerWidthPlusPadding == 0) {
+                    return
+                }
+
+                val relativeScrollX = scrollView.relativeScrollX
+                onMediaScrollingChanged(
+                    relativeScrollX / playerWidthPlusPadding,
+                    relativeScrollX % playerWidthPlusPadding
+                )
             }
-
-            val relativeScrollX = scrollView.relativeScrollX
-            onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding,
-                    relativeScrollX % playerWidthPlusPadding)
         }
-    }
 
-    /**
-     * Whether the media card is visible to user if any
-     */
+    /** Whether the media card is visible to user if any */
     var visibleToUser: Boolean = false
 
-    /**
-     * Whether the quick setting is expanded or not
-     */
+    /** Whether the quick setting is expanded or not */
     var qsExpanded: Boolean = false
 
     init {
@@ -219,47 +187,61 @@
         scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
         mediaContent = scrollView.contentContainer
         scrollView.setOnScrollChangeListener(scrollChangedListener)
-        scrollView.outlineProvider = object : ViewOutlineProvider() {
-            override fun getOutline(view: View?, outline: Outline?) {
-                outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat())
+        scrollView.outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View?, outline: Outline?) {
+                    outline?.setRoundRect(
+                        0,
+                        0,
+                        carouselWidth,
+                        carouselHeight,
+                        cornerRadius.toFloat()
+                    )
+                }
             }
-        }
     }
 
     fun onSettingsButtonUpdated(button: View) {
         settingsButton = button
         // We don't have a context to resolve, lets use the settingsbuttons one since that is
         // reinflated appropriately
-        cornerRadius = settingsButton.resources.getDimensionPixelSize(
-                Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius))
+        cornerRadius =
+            settingsButton.resources.getDimensionPixelSize(
+                Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)
+            )
         updateSettingsPresentation()
         scrollView.invalidateOutline()
     }
 
     private fun updateSettingsPresentation() {
         if (showsSettingsButton && settingsButton.width > 0) {
-            val settingsOffset = MathUtils.map(
+            val settingsOffset =
+                MathUtils.map(
                     0.0f,
                     getMaxTranslation().toFloat(),
                     0.0f,
                     1.0f,
-                    Math.abs(contentTranslation))
-            val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width *
+                    Math.abs(contentTranslation)
+                )
+            val settingsTranslation =
+                (1.0f - settingsOffset) *
+                    -settingsButton.width *
                     SETTINGS_BUTTON_TRANSLATION_FRACTION
-            val newTranslationX = if (isRtl) {
-                // In RTL, the 0-placement is on the right side of the view, not the left...
-                if (contentTranslation > 0) {
-                    -(scrollView.width - settingsTranslation - settingsButton.width)
+            val newTranslationX =
+                if (isRtl) {
+                    // In RTL, the 0-placement is on the right side of the view, not the left...
+                    if (contentTranslation > 0) {
+                        -(scrollView.width - settingsTranslation - settingsButton.width)
+                    } else {
+                        -settingsTranslation
+                    }
                 } else {
-                    -settingsTranslation
+                    if (contentTranslation > 0) {
+                        settingsTranslation
+                    } else {
+                        scrollView.width - settingsTranslation - settingsButton.width
+                    }
                 }
-            } else {
-                if (contentTranslation > 0) {
-                    settingsTranslation
-                } else {
-                    scrollView.width - settingsTranslation - settingsButton.width
-                }
-            }
             val rotation = (1.0f - settingsOffset) * 50
             settingsButton.rotation = rotation * -Math.signum(contentTranslation)
             val alpha = MathUtils.saturate(MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset))
@@ -306,16 +288,14 @@
                 val newScrollX = scrollView.relativeScrollX + dx
                 // Delay the scrolling since scrollView calls springback which cancels
                 // the animation again..
-                mainExecutor.execute {
-                    scrollView.smoothScrollTo(newScrollX, scrollView.scrollY)
-                }
+                mainExecutor.execute { scrollView.smoothScrollTo(newScrollX, scrollView.scrollY) }
             }
             val currentTranslation = scrollView.getContentTranslation()
             if (currentTranslation != 0.0f) {
                 // We started a Swipe but didn't end up with a fling. Let's either go to the
                 // dismissed position or go back.
-                val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 ||
-                        isFalseTouch()
+                val springBack =
+                    Math.abs(currentTranslation) < getMaxTranslation() / 2 || isFalseTouch()
                 val newTranslation: Float
                 if (springBack) {
                     newTranslation = 0.0f
@@ -324,13 +304,17 @@
                     if (!showsSettingsButton) {
                         // Delay the dismiss a bit to avoid too much overlap. Waiting until the
                         // animation has finished also feels a bit too slow here.
-                        mainExecutor.executeDelayed({
-                            dismissCallback.invoke()
-                        }, DISMISS_DELAY)
+                        mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY)
                     }
                 }
-                PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
-                        newTranslation, startVelocity = 0.0f, config = translationConfig).start()
+                PhysicsAnimator.getInstance(this)
+                    .spring(
+                        CONTENT_TRANSLATION,
+                        newTranslation,
+                        startVelocity = 0.0f,
+                        config = translationConfig
+                    )
+                    .start()
                 scrollView.animationTargetX = newTranslation
             }
         }
@@ -338,10 +322,11 @@
         return false
     }
 
-    private fun isFalseTouch() = falsingProtectionNeeded &&
-            falsingManager.isFalseTouch(NOTIFICATION_DISMISS)
+    private fun isFalseTouch() =
+        falsingProtectionNeeded && falsingManager.isFalseTouch(NOTIFICATION_DISMISS)
 
-    private fun getMaxTranslation() = if (showsSettingsButton) {
+    private fun getMaxTranslation() =
+        if (showsSettingsButton) {
             settingsButton.width
         } else {
             playerWidthPlusPadding
@@ -351,15 +336,10 @@
         return gestureDetector.onTouchEvent(motionEvent)
     }
 
-    fun onScroll(
-        down: MotionEvent,
-        lastMotion: MotionEvent,
-        distanceX: Float
-    ): Boolean {
+    fun onScroll(down: MotionEvent, lastMotion: MotionEvent, distanceX: Float): Boolean {
         val totalX = lastMotion.x - down.x
         val currentTranslation = scrollView.getContentTranslation()
-        if (currentTranslation != 0.0f ||
-                !scrollView.canScrollHorizontally((-totalX).toInt())) {
+        if (currentTranslation != 0.0f || !scrollView.canScrollHorizontally((-totalX).toInt())) {
             var newTranslation = currentTranslation - distanceX
             val absTranslation = Math.abs(newTranslation)
             if (absTranslation > getMaxTranslation()) {
@@ -373,14 +353,18 @@
                         newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR
                     } else {
                         // We just crossed the boundary, let's rubberband it all
-                        newTranslation = Math.signum(newTranslation) * (getMaxTranslation() +
-                                (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
+                        newTranslation =
+                            Math.signum(newTranslation) *
+                                (getMaxTranslation() +
+                                    (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
                     }
                 } // Otherwise we don't have do do anything, and will remove the unrubberbanded
                 // translation
             }
-            if (Math.signum(newTranslation) != Math.signum(currentTranslation) &&
-                    currentTranslation != 0.0f) {
+            if (
+                Math.signum(newTranslation) != Math.signum(currentTranslation) &&
+                    currentTranslation != 0.0f
+            ) {
                 // We crossed the 0.0 threshold of the translation. Let's see if we're allowed
                 // to scroll into the new direction
                 if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
@@ -391,8 +375,14 @@
             }
             val physicsAnimator = PhysicsAnimator.getInstance(this)
             if (physicsAnimator.isRunning()) {
-                physicsAnimator.spring(CONTENT_TRANSLATION,
-                        newTranslation, startVelocity = 0.0f, config = translationConfig).start()
+                physicsAnimator
+                    .spring(
+                        CONTENT_TRANSLATION,
+                        newTranslation,
+                        startVelocity = 0.0f,
+                        config = translationConfig
+                    )
+                    .start()
             } else {
                 contentTranslation = newTranslation
             }
@@ -402,10 +392,7 @@
         return false
     }
 
-    private fun onFling(
-        vX: Float,
-        vY: Float
-    ): Boolean {
+    private fun onFling(vX: Float, vY: Float): Boolean {
         if (vX * vX < 0.5 * vY * vY) {
             return false
         }
@@ -424,13 +411,17 @@
                 // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation
                 // has finished also feels a bit too slow here.
                 if (!showsSettingsButton) {
-                    mainExecutor.executeDelayed({
-                        dismissCallback.invoke()
-                    }, DISMISS_DELAY)
+                    mainExecutor.executeDelayed({ dismissCallback.invoke() }, DISMISS_DELAY)
                 }
             }
-            PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
-                    newTranslation, startVelocity = vX, config = translationConfig).start()
+            PhysicsAnimator.getInstance(this)
+                .spring(
+                    CONTENT_TRANSLATION,
+                    newTranslation,
+                    startVelocity = vX,
+                    config = translationConfig
+                )
+                .start()
             scrollView.animationTargetX = newTranslation
         } else {
             // We're flinging the player! Let's go either to the previous or to the next player
@@ -443,21 +434,18 @@
             val view = mediaContent.getChildAt(destIndex)
             // We need to post this since we're dispatching a touch to the underlying view to cancel
             // but canceling will actually abort the animation.
-            mainExecutor.execute {
-                scrollView.smoothScrollTo(view.left, scrollView.scrollY)
-            }
+            mainExecutor.execute { scrollView.smoothScrollTo(view.left, scrollView.scrollY) }
         }
         return true
     }
 
-    /**
-     * Reset the translation of the players when swiped
-     */
+    /** Reset the translation of the players when swiped */
     fun resetTranslation(animate: Boolean = false) {
         if (scrollView.getContentTranslation() != 0.0f) {
             if (animate) {
-                PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
-                        0.0f, config = translationConfig).start()
+                PhysicsAnimator.getInstance(this)
+                    .spring(CONTENT_TRANSLATION, 0.0f, config = translationConfig)
+                    .start()
                 scrollView.animationTargetX = 0.0f
             } else {
                 PhysicsAnimator.getInstance(this).cancel()
@@ -485,21 +473,22 @@
             closeGuts(false)
             updatePlayerVisibilities()
         }
-        val relativeLocation = visibleMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
-            scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
+        val relativeLocation =
+            visibleMediaIndex.toFloat() +
+                if (playerWidthPlusPadding > 0) scrollInAmount.toFloat() / playerWidthPlusPadding
+                else 0f
         // Fix the location, because PageIndicator does not handle RTL internally
-        val location = if (isRtl) {
-            mediaContent.childCount - relativeLocation - 1
-        } else {
-            relativeLocation
-        }
+        val location =
+            if (isRtl) {
+                mediaContent.childCount - relativeLocation - 1
+            } else {
+                relativeLocation
+            }
         pageIndicator.setLocation(location)
         updateClipToOutline()
     }
 
-    /**
-     * Notified whenever the players or their order has changed
-     */
+    /** Notified whenever the players or their order has changed */
     fun onPlayersChanged() {
         updatePlayerVisibilities()
         updateMediaPaddings()
@@ -529,8 +518,8 @@
     }
 
     /**
-     * Notify that a player will be removed right away. This gives us the opporunity to look
-     * where it was and update our scroll position.
+     * Notify that a player will be removed right away. This gives us the opporunity to look where
+     * it was and update our scroll position.
      */
     fun onPrePlayerRemoved(removed: MediaControlPanel) {
         val removedIndex = mediaContent.indexOfChild(removed.mediaViewHolder?.player)
@@ -550,9 +539,7 @@
         }
     }
 
-    /**
-     * Update the bounds of the carousel
-     */
+    /** Update the bounds of the carousel */
     fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) {
         if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) {
             carouselWidth = currentCarouselWidth
@@ -561,9 +548,7 @@
         }
     }
 
-    /**
-     * Reset the MediaScrollView to the start.
-     */
+    /** Reset the MediaScrollView to the start. */
     fun scrollToStart() {
         scrollView.relativeScrollX = 0
     }
@@ -581,21 +566,22 @@
         val destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
         val view = mediaContent.getChildAt(destIndex)
         // We need to post this to wait for the active player becomes visible.
-        mainExecutor.executeDelayed({
-            scrollView.smoothScrollTo(view.left, scrollView.scrollY)
-        }, SCROLL_DELAY)
+        mainExecutor.executeDelayed(
+            { scrollView.smoothScrollTo(view.left, scrollView.scrollY) },
+            SCROLL_DELAY
+        )
     }
 
     companion object {
-        private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>(
-                "contentTranslation") {
-            override fun getValue(handler: MediaCarouselScrollHandler): Float {
-                return handler.contentTranslation
-            }
+        private val CONTENT_TRANSLATION =
+            object : FloatPropertyCompat<MediaCarouselScrollHandler>("contentTranslation") {
+                override fun getValue(handler: MediaCarouselScrollHandler): Float {
+                    return handler.contentTranslation
+                }
 
-            override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
-                handler.contentTranslation = value
+                override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
+                    handler.contentTranslation = value
+                }
             }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
index 208766d..82abf9b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaColorSchemes.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaColorSchemes.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import com.android.systemui.monet.ColorScheme
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index fba51dd..18ecadb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.ui;
 
 import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
 
-import static com.android.systemui.media.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
+import static com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
 
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
@@ -76,6 +76,20 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.media.controls.models.GutsViewHolder;
+import com.android.systemui.media.controls.models.player.MediaAction;
+import com.android.systemui.media.controls.models.player.MediaButton;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.player.MediaDeviceData;
+import com.android.systemui.media.controls.models.player.MediaViewHolder;
+import com.android.systemui.media.controls.models.player.SeekBarObserver;
+import com.android.systemui.media.controls.models.player.SeekBarViewModel;
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder;
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.util.MediaDataUtils;
+import com.android.systemui.media.controls.util.MediaUiEventLogger;
+import com.android.systemui.media.controls.util.SmallHash;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.monet.Style;
@@ -111,7 +125,6 @@
             "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT";
     private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name";
     private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
-    protected static final String KEY_SMARTSPACE_APP_NAME = "KEY_SMARTSPACE_APP_NAME";
 
     // Event types logged by smartspace
     private static final int SMARTSPACE_CARD_CLICK_EVENT = 760;
@@ -251,6 +264,9 @@
         });
     }
 
+    /**
+     * Clean up seekbar and controller when panel is destroyed
+     */
     public void onDestroy() {
         if (mSeekBarObserver != null) {
             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
@@ -651,7 +667,7 @@
             return;
         }
 
-       CharSequence contentDescription;
+        CharSequence contentDescription;
         if (mMediaViewController.isGutsVisible()) {
             contentDescription =
                     mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
new file mode 100644
index 0000000..6b46d8f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt
@@ -0,0 +1,1272 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.annotation.IntDef
+import android.content.Context
+import android.content.res.Configuration
+import android.database.ContentObserver
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.util.MathUtils
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import androidx.annotation.VisibleForTesting
+import com.android.keyguard.KeyguardViewController
+import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dreams.DreamOverlayStateController
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.media.dream.MediaDreamComplication
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.NotifPanelEvents
+import com.android.systemui.statusbar.CrossFadeHelper
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+private val TAG: String = MediaHierarchyManager::class.java.simpleName
+
+/** Similarly to isShown but also excludes views that have 0 alpha */
+val View.isShownNotFaded: Boolean
+    get() {
+        var current: View = this
+        while (true) {
+            if (current.visibility != View.VISIBLE) {
+                return false
+            }
+            if (current.alpha == 0.0f) {
+                return false
+            }
+            val parent = current.parent ?: return false // We are not attached to the view root
+            if (parent !is View) {
+                // we reached the viewroot, hurray
+                return true
+            }
+            current = parent
+        }
+    }
+
+/**
+ * This manager is responsible for placement of the unique media view between the different hosts
+ * and animate the positions of the views to achieve seamless transitions.
+ */
+@SysUISingleton
+class MediaHierarchyManager
+@Inject
+constructor(
+    private val context: Context,
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val keyguardStateController: KeyguardStateController,
+    private val bypassController: KeyguardBypassController,
+    private val mediaCarouselController: MediaCarouselController,
+    private val keyguardViewController: KeyguardViewController,
+    private val dreamOverlayStateController: DreamOverlayStateController,
+    configurationController: ConfigurationController,
+    wakefulnessLifecycle: WakefulnessLifecycle,
+    panelEventsEvents: NotifPanelEvents,
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
+) {
+
+    /** Track the media player setting status on lock screen. */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+        secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
+     * Whether we "skip" QQS during panel expansion.
+     *
+     * This means that when expanding the panel we go directly to QS. Also when we are on QS and
+     * start closing the panel, it fully collapses instead of going to QQS.
+     */
+    private var skipQqsOnExpansion: Boolean = false
+
+    /**
+     * The root overlay of the hierarchy. This is where the media notification is attached to
+     * whenever the view is transitioning from one host to another. It also make sure that the view
+     * is always in its final state when it is attached to a view host.
+     */
+    private var rootOverlay: ViewGroupOverlay? = null
+
+    private var rootView: View? = null
+    private var currentBounds = Rect()
+    private var animationStartBounds: Rect = Rect()
+
+    private var animationStartClipping = Rect()
+    private var currentClipping = Rect()
+    private var targetClipping = Rect()
+
+    /**
+     * The cross fade progress at the start of the animation. 0.5f means it's just switching between
+     * the start and the end location and the content is fully faded, while 0.75f means that we're
+     * halfway faded in again in the target state.
+     */
+    private var animationStartCrossFadeProgress = 0.0f
+
+    /** The starting alpha of the animation */
+    private var animationStartAlpha = 0.0f
+
+    /** The starting location of the cross fade if an animation is running right now. */
+    @MediaLocation private var crossFadeAnimationStartLocation = -1
+
+    /** The end location of the cross fade if an animation is running right now. */
+    @MediaLocation private var crossFadeAnimationEndLocation = -1
+    private var targetBounds: Rect = Rect()
+    private val mediaFrame
+        get() = mediaCarouselController.mediaFrame
+    private var statusbarState: Int = statusBarStateController.state
+    private var animator =
+        ValueAnimator.ofFloat(0.0f, 1.0f).apply {
+            interpolator = Interpolators.FAST_OUT_SLOW_IN
+            addUpdateListener {
+                updateTargetState()
+                val currentAlpha: Float
+                var boundsProgress = animatedFraction
+                if (isCrossFadeAnimatorRunning) {
+                    animationCrossFadeProgress =
+                        MathUtils.lerp(animationStartCrossFadeProgress, 1.0f, animatedFraction)
+                    // When crossfading, let's keep the bounds at the right location during fading
+                    boundsProgress = if (animationCrossFadeProgress < 0.5f) 0.0f else 1.0f
+                    currentAlpha = calculateAlphaFromCrossFade(animationCrossFadeProgress)
+                } else {
+                    // If we're not crossfading, let's interpolate from the start alpha to 1.0f
+                    currentAlpha = MathUtils.lerp(animationStartAlpha, 1.0f, animatedFraction)
+                }
+                interpolateBounds(
+                    animationStartBounds,
+                    targetBounds,
+                    boundsProgress,
+                    result = currentBounds
+                )
+                resolveClipping(currentClipping)
+                applyState(currentBounds, currentAlpha, clipBounds = currentClipping)
+            }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    private var cancelled: Boolean = false
+
+                    override fun onAnimationCancel(animation: Animator?) {
+                        cancelled = true
+                        animationPending = false
+                        rootView?.removeCallbacks(startAnimation)
+                    }
+
+                    override fun onAnimationEnd(animation: Animator?) {
+                        isCrossFadeAnimatorRunning = false
+                        if (!cancelled) {
+                            applyTargetStateIfNotAnimating()
+                        }
+                    }
+
+                    override fun onAnimationStart(animation: Animator?) {
+                        cancelled = false
+                        animationPending = false
+                    }
+                }
+            )
+        }
+
+    private fun resolveClipping(result: Rect) {
+        if (animationStartClipping.isEmpty) result.set(targetClipping)
+        else if (targetClipping.isEmpty) result.set(animationStartClipping)
+        else result.setIntersect(animationStartClipping, targetClipping)
+    }
+
+    private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1)
+    /**
+     * The last location where this view was at before going to the desired location. This is useful
+     * for guided transitions.
+     */
+    @MediaLocation private var previousLocation = -1
+    /** The desired location where the view will be at the end of the transition. */
+    @MediaLocation private var desiredLocation = -1
+
+    /**
+     * The current attachment location where the view is currently attached. Usually this matches
+     * the desired location except for animations whenever a view moves to the new desired location,
+     * during which it is in [IN_OVERLAY].
+     */
+    @MediaLocation private var currentAttachmentLocation = -1
+
+    private var inSplitShade = false
+
+    /** Is there any active media in the carousel? */
+    private var hasActiveMedia: Boolean = false
+        get() = mediaHosts.get(LOCATION_QQS)?.visible == true
+
+    /** Are we currently waiting on an animation to start? */
+    private var animationPending: Boolean = false
+    private val startAnimation: Runnable = Runnable { animator.start() }
+
+    /** The expansion of quick settings */
+    var qsExpansion: Float = 0.0f
+        set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation()
+                if (getQSTransformationProgress() >= 0) {
+                    updateTargetState()
+                    applyTargetStateIfNotAnimating()
+                }
+            }
+        }
+
+    /** Is quick setting expanded? */
+    var qsExpanded: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                mediaCarouselController.mediaCarouselScrollHandler.qsExpanded = value
+            }
+            // qs is expanded on LS shade and HS shade
+            if (value && (isLockScreenShadeVisibleToUser() || isHomeScreenShadeVisibleToUser())) {
+                mediaCarouselController.logSmartspaceImpression(value)
+            }
+            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
+        }
+
+    /**
+     * distance that the full shade transition takes in order for media to fully transition to the
+     * shade
+     */
+    private var distanceForFullShadeTransition = 0
+
+    /**
+     * The amount of progress we are currently in if we're transitioning to the full shade. 0.0f
+     * means we're not transitioning yet, while 1 means we're all the way in the full shade.
+     */
+    private var fullShadeTransitionProgress = 0f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            if (bypassController.bypassEnabled || statusbarState != StatusBarState.KEYGUARD) {
+                // No need to do all the calculations / updates below if we're not on the lockscreen
+                // or if we're bypassing.
+                return
+            }
+            updateDesiredLocation(forceNoAnimation = isCurrentlyFading())
+            if (value >= 0) {
+                updateTargetState()
+                // Setting the alpha directly, as the below call will use it to update the alpha
+                carouselAlpha = calculateAlphaFromCrossFade(field)
+                applyTargetStateIfNotAnimating()
+            }
+        }
+
+    /** Is there currently a cross-fade animation running driven by an animator? */
+    private var isCrossFadeAnimatorRunning = false
+
+    /**
+     * Are we currently transitionioning from the lockscreen to the full shade
+     * [StatusBarState.SHADE_LOCKED] or [StatusBarState.SHADE]. Once the user has dragged down and
+     * the transition starts, this will no longer return true.
+     */
+    private val isTransitioningToFullShade: Boolean
+        get() =
+            fullShadeTransitionProgress != 0f &&
+                !bypassController.bypassEnabled &&
+                statusbarState == StatusBarState.KEYGUARD
+
+    /**
+     * Set the amount of pixels we have currently dragged down if we're transitioning to the full
+     * shade. 0.0f means we're not transitioning yet.
+     */
+    fun setTransitionToFullShadeAmount(value: Float) {
+        // If we're transitioning starting on the shade_locked, we don't want any delay and rather
+        // have it aligned with the rest of the animation
+        val progress = MathUtils.saturate(value / distanceForFullShadeTransition)
+        fullShadeTransitionProgress = progress
+    }
+
+    /**
+     * Returns the amount of translationY of the media container, during the current guided
+     * transformation, if running. If there is no guided transformation running, it will return 0.
+     */
+    fun getGuidedTransformationTranslationY(): Int {
+        if (!isCurrentlyInGuidedTransformation()) {
+            return -1
+        }
+        val startHost = getHost(previousLocation) ?: return 0
+        return targetBounds.top - startHost.currentBounds.top
+    }
+
+    /**
+     * Is the shade currently collapsing from the expanded qs? If we're on the lockscreen and in qs,
+     * we wouldn't want to transition in that case.
+     */
+    var collapsingShadeFromQS: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation(forceNoAnimation = true)
+            }
+        }
+
+    /** Are location changes currently blocked? */
+    private val blockLocationChanges: Boolean
+        get() {
+            return goingToSleep || dozeAnimationRunning
+        }
+
+    /** Are we currently going to sleep */
+    private var goingToSleep: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                if (!value) {
+                    updateDesiredLocation()
+                }
+            }
+        }
+
+    /** Are we currently fullyAwake */
+    private var fullyAwake: Boolean = false
+        set(value) {
+            if (field != value) {
+                field = value
+                if (value) {
+                    updateDesiredLocation(forceNoAnimation = true)
+                }
+            }
+        }
+
+    /** Is the doze animation currently Running */
+    private var dozeAnimationRunning: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                if (!value) {
+                    updateDesiredLocation()
+                }
+            }
+        }
+
+    /** Is the dream overlay currently active */
+    private var dreamOverlayActive: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation(forceNoAnimation = true)
+            }
+        }
+
+    /** Is the dream media complication currently active */
+    private var dreamMediaComplicationActive: Boolean = false
+        private set(value) {
+            if (field != value) {
+                field = value
+                updateDesiredLocation(forceNoAnimation = true)
+            }
+        }
+
+    /**
+     * The current cross fade progress. 0.5f means it's just switching between the start and the end
+     * location and the content is fully faded, while 0.75f means that we're halfway faded in again
+     * in the target state. This is only valid while [isCrossFadeAnimatorRunning] is true.
+     */
+    private var animationCrossFadeProgress = 1.0f
+
+    /** The current carousel Alpha. */
+    private var carouselAlpha: Float = 1.0f
+        set(value) {
+            if (field == value) {
+                return
+            }
+            field = value
+            CrossFadeHelper.fadeIn(mediaFrame, value)
+        }
+
+    /**
+     * Calculate the alpha of the view when given a cross-fade progress.
+     *
+     * @param crossFadeProgress The current cross fade progress. 0.5f means it's just switching
+     * between the start and the end location and the content is fully faded, while 0.75f means that
+     * we're halfway faded in again in the target state.
+     */
+    private fun calculateAlphaFromCrossFade(crossFadeProgress: Float): Float {
+        if (crossFadeProgress <= 0.5f) {
+            return 1.0f - crossFadeProgress / 0.5f
+        } else {
+            return (crossFadeProgress - 0.5f) / 0.5f
+        }
+    }
+
+    init {
+        updateConfiguration()
+        configurationController.addCallback(
+            object : ConfigurationController.ConfigurationListener {
+                override fun onConfigChanged(newConfig: Configuration?) {
+                    updateConfiguration()
+                    updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = true)
+                }
+            }
+        )
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onStatePreChange(oldState: Int, newState: Int) {
+                    // We're updating the location before the state change happens, since we want
+                    // the
+                    // location of the previous state to still be up to date when the animation
+                    // starts
+                    statusbarState = newState
+                    updateDesiredLocation()
+                }
+
+                override fun onStateChanged(newState: Int) {
+                    updateTargetState()
+                    // Enters shade from lock screen
+                    if (
+                        newState == StatusBarState.SHADE_LOCKED && isLockScreenShadeVisibleToUser()
+                    ) {
+                        mediaCarouselController.logSmartspaceImpression(qsExpanded)
+                    }
+                    mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+                        isVisibleToUser()
+                }
+
+                override fun onDozeAmountChanged(linear: Float, eased: Float) {
+                    dozeAnimationRunning = linear != 0.0f && linear != 1.0f
+                }
+
+                override fun onDozingChanged(isDozing: Boolean) {
+                    if (!isDozing) {
+                        dozeAnimationRunning = false
+                        // Enters lock screen from screen off
+                        if (isLockScreenVisibleToUser()) {
+                            mediaCarouselController.logSmartspaceImpression(qsExpanded)
+                        }
+                    } else {
+                        updateDesiredLocation()
+                        qsExpanded = false
+                        closeGuts()
+                    }
+                    mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+                        isVisibleToUser()
+                }
+
+                override fun onExpandedChanged(isExpanded: Boolean) {
+                    // Enters shade from home screen
+                    if (isHomeScreenShadeVisibleToUser()) {
+                        mediaCarouselController.logSmartspaceImpression(qsExpanded)
+                    }
+                    mediaCarouselController.mediaCarouselScrollHandler.visibleToUser =
+                        isVisibleToUser()
+                }
+            }
+        )
+
+        dreamOverlayStateController.addCallback(
+            object : DreamOverlayStateController.Callback {
+                override fun onComplicationsChanged() {
+                    dreamMediaComplicationActive =
+                        dreamOverlayStateController.complications.any {
+                            it is MediaDreamComplication
+                        }
+                }
+
+                override fun onStateChanged() {
+                    dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
+                }
+            }
+        )
+
+        wakefulnessLifecycle.addObserver(
+            object : WakefulnessLifecycle.Observer {
+                override fun onFinishedGoingToSleep() {
+                    goingToSleep = false
+                }
+
+                override fun onStartedGoingToSleep() {
+                    goingToSleep = true
+                    fullyAwake = false
+                }
+
+                override fun onFinishedWakingUp() {
+                    goingToSleep = false
+                    fullyAwake = true
+                }
+
+                override fun onStartedWakingUp() {
+                    goingToSleep = false
+                }
+            }
+        )
+
+        mediaCarouselController.updateUserVisibility = {
+            mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
+        }
+        mediaCarouselController.updateHostVisibility = {
+            mediaHosts.forEach { it?.updateViewVisibility() }
+        }
+
+        panelEventsEvents.registerListener(
+            object : NotifPanelEvents.Listener {
+                override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
+                    skipQqsOnExpansion = isExpandImmediateEnabled
+                    updateDesiredLocation()
+                }
+            }
+        )
+
+        val settingsObserver: ContentObserver =
+            object : ContentObserver(handler) {
+                override fun onChange(selfChange: Boolean, uri: Uri?) {
+                    if (uri == lockScreenMediaPlayerUri) {
+                        allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                true,
+                                UserHandle.USER_CURRENT
+                            )
+                    }
+                }
+            }
+        secureSettings.registerContentObserverForUser(
+            Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+            settingsObserver,
+            UserHandle.USER_ALL
+        )
+    }
+
+    private fun updateConfiguration() {
+        distanceForFullShadeTransition =
+            context.resources.getDimensionPixelSize(
+                R.dimen.lockscreen_shade_media_transition_distance
+            )
+        inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
+    }
+
+    /**
+     * Register a media host and create a view can be attached to a view hierarchy and where the
+     * players will be placed in when the host is the currently desired state.
+     *
+     * @return the hostView associated with this location
+     */
+    fun register(mediaObject: MediaHost): UniqueObjectHostView {
+        val viewHost = createUniqueObjectHost()
+        mediaObject.hostView = viewHost
+        mediaObject.addVisibilityChangeListener {
+            // If QQS changes visibility, we need to force an update to ensure the transition
+            // goes into the correct state
+            val stateUpdate = mediaObject.location == LOCATION_QQS
+
+            // Never animate because of a visibility change, only state changes should do that
+            updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate)
+        }
+        mediaHosts[mediaObject.location] = mediaObject
+        if (mediaObject.location == desiredLocation) {
+            // In case we are overriding a view that is already visible, make sure we attach it
+            // to this new host view in the below call
+            desiredLocation = -1
+        }
+        if (mediaObject.location == currentAttachmentLocation) {
+            currentAttachmentLocation = -1
+        }
+        updateDesiredLocation()
+        return viewHost
+    }
+
+    /** Close the guts in all players in [MediaCarouselController]. */
+    fun closeGuts() {
+        mediaCarouselController.closeGuts()
+    }
+
+    private fun createUniqueObjectHost(): UniqueObjectHostView {
+        val viewHost = UniqueObjectHostView(context)
+        viewHost.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(p0: View?) {
+                    if (rootOverlay == null) {
+                        rootView = viewHost.viewRootImpl.view
+                        rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
+                    }
+                    viewHost.removeOnAttachStateChangeListener(this)
+                }
+
+                override fun onViewDetachedFromWindow(p0: View?) {}
+            }
+        )
+        return viewHost
+    }
+
+    /**
+     * Updates the location that the view should be in. If it changes, an animation may be triggered
+     * going from the old desired location to the new one.
+     *
+     * @param forceNoAnimation optional parameter telling the system not to animate
+     * @param forceStateUpdate optional parameter telling the system to update transition state
+     * ```
+     *                         even if location did not change
+     * ```
+     */
+    private fun updateDesiredLocation(
+        forceNoAnimation: Boolean = false,
+        forceStateUpdate: Boolean = false
+    ) =
+        traceSection("MediaHierarchyManager#updateDesiredLocation") {
+            val desiredLocation = calculateLocation()
+            if (desiredLocation != this.desiredLocation || forceStateUpdate) {
+                if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
+                    // Only update previous location when it actually changes
+                    previousLocation = this.desiredLocation
+                } else if (forceStateUpdate) {
+                    val onLockscreen =
+                        (!bypassController.bypassEnabled &&
+                            (statusbarState == StatusBarState.KEYGUARD))
+                    if (
+                        desiredLocation == LOCATION_QS &&
+                            previousLocation == LOCATION_LOCKSCREEN &&
+                            !onLockscreen
+                    ) {
+                        // If media active state changed and the device is now unlocked, update the
+                        // previous location so we animate between the correct hosts
+                        previousLocation = LOCATION_QQS
+                    }
+                }
+                val isNewView = this.desiredLocation == -1
+                this.desiredLocation = desiredLocation
+                // Let's perform a transition
+                val animate =
+                    !forceNoAnimation && shouldAnimateTransition(desiredLocation, previousLocation)
+                val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+                val host = getHost(desiredLocation)
+                val willFade = calculateTransformationType() == TRANSFORMATION_TYPE_FADE
+                if (!willFade || isCurrentlyInGuidedTransformation() || !animate) {
+                    // if we're fading, we want the desired location / measurement only to change
+                    // once fully faded. This is happening in the host attachment
+                    mediaCarouselController.onDesiredLocationChanged(
+                        desiredLocation,
+                        host,
+                        animate,
+                        animDuration,
+                        delay
+                    )
+                }
+                performTransitionToNewLocation(isNewView, animate)
+            }
+        }
+
+    private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) =
+        traceSection("MediaHierarchyManager#performTransitionToNewLocation") {
+            if (previousLocation < 0 || isNewView) {
+                cancelAnimationAndApplyDesiredState()
+                return
+            }
+            val currentHost = getHost(desiredLocation)
+            val previousHost = getHost(previousLocation)
+            if (currentHost == null || previousHost == null) {
+                cancelAnimationAndApplyDesiredState()
+                return
+            }
+            updateTargetState()
+            if (isCurrentlyInGuidedTransformation()) {
+                applyTargetStateIfNotAnimating()
+            } else if (animate) {
+                val wasCrossFading = isCrossFadeAnimatorRunning
+                val previewsCrossFadeProgress = animationCrossFadeProgress
+                animator.cancel()
+                if (
+                    currentAttachmentLocation != previousLocation ||
+                        !previousHost.hostView.isAttachedToWindow
+                ) {
+                    // Let's animate to the new position, starting from the current position
+                    // We also go in here in case the view was detached, since the bounds wouldn't
+                    // be correct anymore
+                    animationStartBounds.set(currentBounds)
+                    animationStartClipping.set(currentClipping)
+                } else {
+                    // otherwise, let's take the freshest state, since the current one could
+                    // be outdated
+                    animationStartBounds.set(previousHost.currentBounds)
+                    animationStartClipping.set(previousHost.currentClipping)
+                }
+                val transformationType = calculateTransformationType()
+                var needsCrossFade = transformationType == TRANSFORMATION_TYPE_FADE
+                var crossFadeStartProgress = 0.0f
+                // The alpha is only relevant when not cross fading
+                var newCrossFadeStartLocation = previousLocation
+                if (wasCrossFading) {
+                    if (currentAttachmentLocation == crossFadeAnimationEndLocation) {
+                        if (needsCrossFade) {
+                            // We were previously crossFading and we've already reached
+                            // the end view, Let's start crossfading from the same position there
+                            crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
+                        }
+                        // Otherwise let's fade in from the current alpha, but not cross fade
+                    } else {
+                        // We haven't reached the previous location yet, let's still cross fade from
+                        // where we were.
+                        newCrossFadeStartLocation = crossFadeAnimationStartLocation
+                        if (newCrossFadeStartLocation == desiredLocation) {
+                            // we're crossFading back to where we were, let's start at the end
+                            // position
+                            crossFadeStartProgress = 1.0f - previewsCrossFadeProgress
+                        } else {
+                            // Let's start from where we are right now
+                            crossFadeStartProgress = previewsCrossFadeProgress
+                            // We need to force cross fading as we haven't reached the end location
+                            // yet
+                            needsCrossFade = true
+                        }
+                    }
+                } else if (needsCrossFade) {
+                    // let's not flicker and start with the same alpha
+                    crossFadeStartProgress = (1.0f - carouselAlpha) / 2.0f
+                }
+                isCrossFadeAnimatorRunning = needsCrossFade
+                crossFadeAnimationStartLocation = newCrossFadeStartLocation
+                crossFadeAnimationEndLocation = desiredLocation
+                animationStartAlpha = carouselAlpha
+                animationStartCrossFadeProgress = crossFadeStartProgress
+                adjustAnimatorForTransition(desiredLocation, previousLocation)
+                if (!animationPending) {
+                    rootView?.let {
+                        // Let's delay the animation start until we finished laying out
+                        animationPending = true
+                        it.postOnAnimation(startAnimation)
+                    }
+                }
+            } else {
+                cancelAnimationAndApplyDesiredState()
+            }
+        }
+
+    private fun shouldAnimateTransition(
+        @MediaLocation currentLocation: Int,
+        @MediaLocation previousLocation: Int
+    ): Boolean {
+        if (isCurrentlyInGuidedTransformation()) {
+            return false
+        }
+        if (skipQqsOnExpansion) {
+            return false
+        }
+        // This is an invalid transition, and can happen when using the camera gesture from the
+        // lock screen. Disallow.
+        if (
+            previousLocation == LOCATION_LOCKSCREEN &&
+                desiredLocation == LOCATION_QQS &&
+                statusbarState == StatusBarState.SHADE
+        ) {
+            return false
+        }
+
+        if (
+            currentLocation == LOCATION_QQS &&
+                previousLocation == LOCATION_LOCKSCREEN &&
+                (statusBarStateController.leaveOpenOnKeyguardHide() ||
+                    statusbarState == StatusBarState.SHADE_LOCKED)
+        ) {
+            // Usually listening to the isShown is enough to determine this, but there is some
+            // non-trivial reattaching logic happening that will make the view not-shown earlier
+            return true
+        }
+
+        if (
+            statusbarState == StatusBarState.KEYGUARD &&
+                (currentLocation == LOCATION_LOCKSCREEN || previousLocation == LOCATION_LOCKSCREEN)
+        ) {
+            // We're always fading from lockscreen to keyguard in situations where the player
+            // is already fully hidden
+            return false
+        }
+        return mediaFrame.isShownNotFaded || animator.isRunning || animationPending
+    }
+
+    private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
+        val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
+        animator.apply {
+            duration = animDuration
+            startDelay = delay
+        }
+    }
+
+    private fun getAnimationParams(previousLocation: Int, desiredLocation: Int): Pair<Long, Long> {
+        var animDuration = 200L
+        var delay = 0L
+        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+            // Going to the full shade, let's adjust the animation duration
+            if (
+                statusbarState == StatusBarState.SHADE &&
+                    keyguardStateController.isKeyguardFadingAway
+            ) {
+                delay = keyguardStateController.keyguardFadingAwayDelay
+            }
+            animDuration = (StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE / 2f).toLong()
+        } else if (previousLocation == LOCATION_QQS && desiredLocation == LOCATION_LOCKSCREEN) {
+            animDuration = StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR.toLong()
+        }
+        return animDuration to delay
+    }
+
+    private fun applyTargetStateIfNotAnimating() {
+        if (!animator.isRunning) {
+            // Let's immediately apply the target state (which is interpolated) if there is
+            // no animation running. Otherwise the animation update will already update
+            // the location
+            applyState(targetBounds, carouselAlpha, clipBounds = targetClipping)
+        }
+    }
+
+    /** Updates the bounds that the view wants to be in at the end of the animation. */
+    private fun updateTargetState() {
+        var starthost = getHost(previousLocation)
+        var endHost = getHost(desiredLocation)
+        if (
+            isCurrentlyInGuidedTransformation() &&
+                !isCurrentlyFading() &&
+                starthost != null &&
+                endHost != null
+        ) {
+            val progress = getTransformationProgress()
+            // If either of the hosts are invisible, let's keep them at the other host location to
+            // have a nicer disappear animation. Otherwise the currentBounds of the state might
+            // be undefined
+            if (!endHost.visible) {
+                endHost = starthost
+            } else if (!starthost.visible) {
+                starthost = endHost
+            }
+            val newBounds = endHost.currentBounds
+            val previousBounds = starthost.currentBounds
+            targetBounds = interpolateBounds(previousBounds, newBounds, progress)
+            targetClipping = endHost.currentClipping
+        } else if (endHost != null) {
+            val bounds = endHost.currentBounds
+            targetBounds.set(bounds)
+            targetClipping = endHost.currentClipping
+        }
+    }
+
+    private fun interpolateBounds(
+        startBounds: Rect,
+        endBounds: Rect,
+        progress: Float,
+        result: Rect? = null
+    ): Rect {
+        val left =
+            MathUtils.lerp(startBounds.left.toFloat(), endBounds.left.toFloat(), progress).toInt()
+        val top =
+            MathUtils.lerp(startBounds.top.toFloat(), endBounds.top.toFloat(), progress).toInt()
+        val right =
+            MathUtils.lerp(startBounds.right.toFloat(), endBounds.right.toFloat(), progress).toInt()
+        val bottom =
+            MathUtils.lerp(startBounds.bottom.toFloat(), endBounds.bottom.toFloat(), progress)
+                .toInt()
+        val resultBounds = result ?: Rect()
+        resultBounds.set(left, top, right, bottom)
+        return resultBounds
+    }
+
+    /** @return true if this transformation is guided by an external progress like a finger */
+    fun isCurrentlyInGuidedTransformation(): Boolean {
+        return hasValidStartAndEndLocations() &&
+            getTransformationProgress() >= 0 &&
+            areGuidedTransitionHostsVisible()
+    }
+
+    private fun hasValidStartAndEndLocations(): Boolean {
+        return previousLocation != -1 && desiredLocation != -1
+    }
+
+    /** Calculate the transformation type for the current animation */
+    @VisibleForTesting
+    @TransformationType
+    fun calculateTransformationType(): Int {
+        if (isTransitioningToFullShade) {
+            if (inSplitShade && areGuidedTransitionHostsVisible()) {
+                return TRANSFORMATION_TYPE_TRANSITION
+            }
+            return TRANSFORMATION_TYPE_FADE
+        }
+        if (
+            previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QS ||
+                previousLocation == LOCATION_QS && desiredLocation == LOCATION_LOCKSCREEN
+        ) {
+            // animating between ls and qs should fade, as QS is clipped.
+            return TRANSFORMATION_TYPE_FADE
+        }
+        if (previousLocation == LOCATION_LOCKSCREEN && desiredLocation == LOCATION_QQS) {
+            // animating between ls and qqs should fade when dragging down via e.g. expand button
+            return TRANSFORMATION_TYPE_FADE
+        }
+        return TRANSFORMATION_TYPE_TRANSITION
+    }
+
+    private fun areGuidedTransitionHostsVisible(): Boolean {
+        return getHost(previousLocation)?.visible == true &&
+            getHost(desiredLocation)?.visible == true
+    }
+
+    /**
+     * @return the current transformation progress if we're in a guided transformation and -1
+     * otherwise
+     */
+    private fun getTransformationProgress(): Float {
+        if (skipQqsOnExpansion) {
+            return -1.0f
+        }
+        val progress = getQSTransformationProgress()
+        if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
+            return progress
+        }
+        if (isTransitioningToFullShade) {
+            return fullShadeTransitionProgress
+        }
+        return -1.0f
+    }
+
+    private fun getQSTransformationProgress(): Float {
+        val currentHost = getHost(desiredLocation)
+        val previousHost = getHost(previousLocation)
+        if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) {
+            if (previousHost?.location == LOCATION_QQS) {
+                if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) {
+                    return qsExpansion
+                }
+            }
+        }
+        return -1.0f
+    }
+
+    private fun getHost(@MediaLocation location: Int): MediaHost? {
+        if (location < 0) {
+            return null
+        }
+        return mediaHosts[location]
+    }
+
+    private fun cancelAnimationAndApplyDesiredState() {
+        animator.cancel()
+        getHost(desiredLocation)?.let {
+            applyState(it.currentBounds, alpha = 1.0f, immediately = true)
+        }
+    }
+
+    /** Apply the current state to the view, updating it's bounds and desired state */
+    private fun applyState(
+        bounds: Rect,
+        alpha: Float,
+        immediately: Boolean = false,
+        clipBounds: Rect = EMPTY_RECT
+    ) =
+        traceSection("MediaHierarchyManager#applyState") {
+            currentBounds.set(bounds)
+            currentClipping = clipBounds
+            carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
+            val onlyUseEndState = !isCurrentlyInGuidedTransformation() || isCurrentlyFading()
+            val startLocation = if (onlyUseEndState) -1 else previousLocation
+            val progress = if (onlyUseEndState) 1.0f else getTransformationProgress()
+            val endLocation = resolveLocationForFading()
+            mediaCarouselController.setCurrentState(
+                startLocation,
+                endLocation,
+                progress,
+                immediately
+            )
+            updateHostAttachment()
+            if (currentAttachmentLocation == IN_OVERLAY) {
+                // Setting the clipping on the hierarchy of `mediaFrame` does not work
+                if (!currentClipping.isEmpty) {
+                    currentBounds.intersect(currentClipping)
+                }
+                mediaFrame.setLeftTopRightBottom(
+                    currentBounds.left,
+                    currentBounds.top,
+                    currentBounds.right,
+                    currentBounds.bottom
+                )
+            }
+        }
+
+    private fun updateHostAttachment() =
+        traceSection("MediaHierarchyManager#updateHostAttachment") {
+            var newLocation = resolveLocationForFading()
+            var canUseOverlay = !isCurrentlyFading()
+            if (isCrossFadeAnimatorRunning) {
+                if (
+                    getHost(newLocation)?.visible == true &&
+                        getHost(newLocation)?.hostView?.isShown == false &&
+                        newLocation != desiredLocation
+                ) {
+                    // We're crossfading but the view is already hidden. Let's move to the overlay
+                    // instead. This happens when animating to the full shade using a button click.
+                    canUseOverlay = true
+                }
+            }
+            val inOverlay = isTransitionRunning() && rootOverlay != null && canUseOverlay
+            newLocation = if (inOverlay) IN_OVERLAY else newLocation
+            if (currentAttachmentLocation != newLocation) {
+                currentAttachmentLocation = newLocation
+
+                // Remove the carousel from the old host
+                (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
+
+                // Add it to the new one
+                if (inOverlay) {
+                    rootOverlay!!.add(mediaFrame)
+                } else {
+                    val targetHost = getHost(newLocation)!!.hostView
+                    // When adding back to the host, let's make sure to reset the bounds.
+                    // Usually adding the view will trigger a layout that does this automatically,
+                    // but we sometimes suppress this.
+                    targetHost.addView(mediaFrame)
+                    val left = targetHost.paddingLeft
+                    val top = targetHost.paddingTop
+                    mediaFrame.setLeftTopRightBottom(
+                        left,
+                        top,
+                        left + currentBounds.width(),
+                        top + currentBounds.height()
+                    )
+
+                    if (mediaFrame.childCount > 0) {
+                        val child = mediaFrame.getChildAt(0)
+                        if (mediaFrame.height < child.height) {
+                            Log.wtf(
+                                TAG,
+                                "mediaFrame height is too small for child: " +
+                                    "${mediaFrame.height} vs ${child.height}"
+                            )
+                        }
+                    }
+                }
+                if (isCrossFadeAnimatorRunning) {
+                    // When cross-fading with an animation, we only notify the media carousel of the
+                    // location change, once the view is reattached to the new place and not
+                    // immediately
+                    // when the desired location changes. This callback will update the measurement
+                    // of the carousel, only once we've faded out at the old location and then
+                    // reattach
+                    // to fade it in at the new location.
+                    mediaCarouselController.onDesiredLocationChanged(
+                        newLocation,
+                        getHost(newLocation),
+                        animate = false
+                    )
+                }
+            }
+        }
+
+    /**
+     * Calculate the location when cross fading between locations. While fading out, the content
+     * should remain in the previous location, while after the switch it should be at the desired
+     * location.
+     */
+    private fun resolveLocationForFading(): Int {
+        if (isCrossFadeAnimatorRunning) {
+            // When animating between two hosts with a fade, let's keep ourselves in the old
+            // location for the first half, and then switch over to the end location
+            if (animationCrossFadeProgress > 0.5 || previousLocation == -1) {
+                return crossFadeAnimationEndLocation
+            } else {
+                return crossFadeAnimationStartLocation
+            }
+        }
+        return desiredLocation
+    }
+
+    private fun isTransitionRunning(): Boolean {
+        return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
+            animator.isRunning ||
+            animationPending
+    }
+
+    @MediaLocation
+    private fun calculateLocation(): Int {
+        if (blockLocationChanges) {
+            // Keep the current location until we're allowed to again
+            return desiredLocation
+        }
+        val onLockscreen =
+            (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD))
+        val location =
+            when {
+                dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
+                (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
+                qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
+                !hasActiveMedia -> LOCATION_QS
+                onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
+                onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
+                onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
+                else -> LOCATION_QQS
+            }
+        // When we're on lock screen and the player is not active, we should keep it in QS.
+        // Otherwise it will try to animate a transition that doesn't make sense.
+        if (
+            location == LOCATION_LOCKSCREEN &&
+                getHost(location)?.visible != true &&
+                !statusBarStateController.isDozing
+        ) {
+            return LOCATION_QS
+        }
+        if (
+            location == LOCATION_LOCKSCREEN &&
+                desiredLocation == LOCATION_QS &&
+                collapsingShadeFromQS
+        ) {
+            // When collapsing on the lockscreen, we want to remain in QS
+            return LOCATION_QS
+        }
+        if (
+            location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && !fullyAwake
+        ) {
+            // When unlocking from dozing / while waking up, the media shouldn't be transitioning
+            // in an animated way. Let's keep it in the lockscreen until we're fully awake and
+            // reattach it without an animation
+            return LOCATION_LOCKSCREEN
+        }
+        if (skipQqsOnExpansion) {
+            // When doing an immediate expand or collapse, we want to keep it in QS.
+            return LOCATION_QS
+        }
+        return location
+    }
+
+    private fun isSplitShadeExpanding(): Boolean {
+        return inSplitShade && isTransitioningToFullShade
+    }
+
+    /** Are we currently transforming to the full shade and already in QQS */
+    private fun isTransformingToFullShadeAndInQQS(): Boolean {
+        if (!isTransitioningToFullShade) {
+            return false
+        }
+        if (inSplitShade) {
+            // Split shade doesn't use QQS.
+            return false
+        }
+        return fullShadeTransitionProgress > 0.5f
+    }
+
+    /** Is the current transformationType fading */
+    private fun isCurrentlyFading(): Boolean {
+        if (isSplitShadeExpanding()) {
+            // Split shade always uses transition instead of fade.
+            return false
+        }
+        if (isTransitioningToFullShade) {
+            return true
+        }
+        return isCrossFadeAnimatorRunning
+    }
+
+    /** Returns true when the media card could be visible to the user if existed. */
+    private fun isVisibleToUser(): Boolean {
+        return isLockScreenVisibleToUser() ||
+            isLockScreenShadeVisibleToUser() ||
+            isHomeScreenShadeVisibleToUser()
+    }
+
+    private fun isLockScreenVisibleToUser(): Boolean {
+        return !statusBarStateController.isDozing &&
+            !keyguardViewController.isBouncerShowing &&
+            statusBarStateController.state == StatusBarState.KEYGUARD &&
+            allowMediaPlayerOnLockScreen &&
+            statusBarStateController.isExpanded &&
+            !qsExpanded
+    }
+
+    private fun isLockScreenShadeVisibleToUser(): Boolean {
+        return !statusBarStateController.isDozing &&
+            !keyguardViewController.isBouncerShowing &&
+            (statusBarStateController.state == StatusBarState.SHADE_LOCKED ||
+                (statusBarStateController.state == StatusBarState.KEYGUARD && qsExpanded))
+    }
+
+    private fun isHomeScreenShadeVisibleToUser(): Boolean {
+        return !statusBarStateController.isDozing &&
+            statusBarStateController.state == StatusBarState.SHADE &&
+            statusBarStateController.isExpanded
+    }
+
+    companion object {
+        /** Attached in expanded quick settings */
+        const val LOCATION_QS = 0
+
+        /** Attached in the collapsed QS */
+        const val LOCATION_QQS = 1
+
+        /** Attached on the lock screen */
+        const val LOCATION_LOCKSCREEN = 2
+
+        /** Attached on the dream overlay */
+        const val LOCATION_DREAM_OVERLAY = 3
+
+        /** Attached at the root of the hierarchy in an overlay */
+        const val IN_OVERLAY = -1000
+
+        /**
+         * The default transformation type where the hosts transform into each other using a direct
+         * transition
+         */
+        const val TRANSFORMATION_TYPE_TRANSITION = 0
+
+        /**
+         * A transformation type where content fades from one place to another instead of
+         * transitioning
+         */
+        const val TRANSFORMATION_TYPE_FADE = 1
+    }
+}
+
+private val EMPTY_RECT = Rect()
+
+@IntDef(
+    prefix = ["TRANSFORMATION_TYPE_"],
+    value =
+        [
+            MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION,
+            MediaHierarchyManager.TRANSFORMATION_TYPE_FADE
+        ]
+)
+@Retention(AnnotationRetention.SOURCE)
+private annotation class TransformationType
+
+@IntDef(
+    prefix = ["LOCATION_"],
+    value =
+        [
+            MediaHierarchyManager.LOCATION_QS,
+            MediaHierarchyManager.LOCATION_QQS,
+            MediaHierarchyManager.LOCATION_LOCKSCREEN,
+            MediaHierarchyManager.LOCATION_DREAM_OVERLAY
+        ]
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class MediaLocation
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
similarity index 62%
rename from packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
index 8645922..455b7de 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt
@@ -1,9 +1,28 @@
-package com.android.systemui.media
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
 
 import android.graphics.Rect
 import android.util.ArraySet
 import android.view.View
 import android.view.View.OnAttachStateChangeListener
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.util.animation.DisappearParameters
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.MeasurementOutput
@@ -11,7 +30,8 @@
 import java.util.Objects
 import javax.inject.Inject
 
-class MediaHost constructor(
+class MediaHost
+constructor(
     private val state: MediaHostStateHolder,
     private val mediaHierarchyManager: MediaHierarchyManager,
     private val mediaDataManager: MediaDataManager,
@@ -26,14 +46,10 @@
 
     private var inited: Boolean = false
 
-    /**
-     * Are we listening to media data changes?
-     */
+    /** Are we listening to media data changes? */
     private var listeningToMediaData = false
 
-    /**
-     * Get the current bounds on the screen. This makes sure the state is fresh and up to date
-     */
+    /** Get the current bounds on the screen. This makes sure the state is fresh and up to date */
     val currentBounds: Rect = Rect()
         get() {
             hostView.getLocationOnScreen(tmpLocationOnScreen)
@@ -62,38 +78,39 @@
      */
     val currentClipping = Rect()
 
-    private val listener = object : MediaDataManager.Listener {
-        override fun onMediaDataLoaded(
-            key: String,
-            oldKey: String?,
-            data: MediaData,
-            immediately: Boolean,
-            receivedSmartspaceCardLatency: Int,
-            isSsReactivated: Boolean
-        ) {
-            if (immediately) {
+    private val listener =
+        object : MediaDataManager.Listener {
+            override fun onMediaDataLoaded(
+                key: String,
+                oldKey: String?,
+                data: MediaData,
+                immediately: Boolean,
+                receivedSmartspaceCardLatency: Int,
+                isSsReactivated: Boolean
+            ) {
+                if (immediately) {
+                    updateViewVisibility()
+                }
+            }
+
+            override fun onSmartspaceMediaDataLoaded(
+                key: String,
+                data: SmartspaceMediaData,
+                shouldPrioritize: Boolean
+            ) {
                 updateViewVisibility()
             }
-        }
 
-        override fun onSmartspaceMediaDataLoaded(
-            key: String,
-            data: SmartspaceMediaData,
-            shouldPrioritize: Boolean
-        ) {
-            updateViewVisibility()
-        }
-
-        override fun onMediaDataRemoved(key: String) {
-            updateViewVisibility()
-        }
-
-        override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
-            if (immediately) {
+            override fun onMediaDataRemoved(key: String) {
                 updateViewVisibility()
             }
+
+            override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
+                if (immediately) {
+                    updateViewVisibility()
+                }
+            }
         }
-    }
 
     fun addVisibilityChangeListener(listener: (Boolean) -> Unit) {
         visibleChangedListeners.add(listener)
@@ -104,12 +121,14 @@
     }
 
     /**
-     * Initialize this MediaObject and create a host view.
-     * All state should already be set on this host before calling this method in order to avoid
-     * unnecessary state changes which lead to remeasurings later on.
+     * Initialize this MediaObject and create a host view. All state should already be set on this
+     * host before calling this method in order to avoid unnecessary state changes which lead to
+     * remeasurings later on.
      *
      * @param location the location this host name has. Used to identify the host during
+     * ```
      *                 transitions.
+     * ```
      */
     fun init(@MediaLocation location: Int) {
         if (inited) {
@@ -122,36 +141,42 @@
         // Listen by default, as the host might not be attached by our clients, until
         // they get a visibility change. We still want to stay up to date in that case!
         setListeningToMediaData(true)
-        hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
-            override fun onViewAttachedToWindow(v: View?) {
-                setListeningToMediaData(true)
-                updateViewVisibility()
-            }
+        hostView.addOnAttachStateChangeListener(
+            object : OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(v: View?) {
+                    setListeningToMediaData(true)
+                    updateViewVisibility()
+                }
 
-            override fun onViewDetachedFromWindow(v: View?) {
-                setListeningToMediaData(false)
+                override fun onViewDetachedFromWindow(v: View?) {
+                    setListeningToMediaData(false)
+                }
             }
-        })
+        )
 
         // Listen to measurement updates and update our state with it
-        hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
-            override fun onMeasure(input: MeasurementInput): MeasurementOutput {
-                // Modify the measurement to exactly match the dimensions
-                if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
-                    input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
-                            View.MeasureSpec.getSize(input.widthMeasureSpec),
-                            View.MeasureSpec.EXACTLY)
+        hostView.measurementManager =
+            object : UniqueObjectHostView.MeasurementManager {
+                override fun onMeasure(input: MeasurementInput): MeasurementOutput {
+                    // Modify the measurement to exactly match the dimensions
+                    if (
+                        View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST
+                    ) {
+                        input.widthMeasureSpec =
+                            View.MeasureSpec.makeMeasureSpec(
+                                View.MeasureSpec.getSize(input.widthMeasureSpec),
+                                View.MeasureSpec.EXACTLY
+                            )
+                    }
+                    // This will trigger a state change that ensures that we now have a state
+                    // available
+                    state.measurementInput = input
+                    return mediaHostStatesManager.updateCarouselDimensions(location, state)
                 }
-                // This will trigger a state change that ensures that we now have a state available
-                state.measurementInput = input
-                return mediaHostStatesManager.updateCarouselDimensions(location, state)
             }
-        }
 
         // Whenever the state changes, let our state manager know
-        state.changedListener = {
-            mediaHostStatesManager.updateHostState(location, state)
-        }
+        state.changedListener = { mediaHostStatesManager.updateHostState(location, state) }
 
         updateViewVisibility()
     }
@@ -172,17 +197,16 @@
      * the visibility has changed
      */
     fun updateViewVisibility() {
-        state.visible = if (showsOnlyActiveMedia) {
-            mediaDataManager.hasActiveMediaOrRecommendation()
-        } else {
-            mediaDataManager.hasAnyMediaOrRecommendation()
-        }
+        state.visible =
+            if (showsOnlyActiveMedia) {
+                mediaDataManager.hasActiveMediaOrRecommendation()
+            } else {
+                mediaDataManager.hasAnyMediaOrRecommendation()
+            }
         val newVisibility = if (visible) View.VISIBLE else View.GONE
         if (newVisibility != hostView.visibility) {
             hostView.visibility = newVisibility
-            visibleChangedListeners.forEach {
-                it.invoke(visible)
-            }
+            visibleChangedListeners.forEach { it.invoke(visible) }
         }
     }
 
@@ -250,14 +274,10 @@
 
         private var lastDisappearHash = disappearParameters.hashCode()
 
-        /**
-         * A listener for all changes. This won't be copied over when invoking [copy]
-         */
+        /** A listener for all changes. This won't be copied over when invoking [copy] */
         var changedListener: (() -> Unit)? = null
 
-        /**
-         * Get a copy of this state. This won't copy any listeners it may have set
-         */
+        /** Get a copy of this state. This won't copy any listeners it may have set */
         override fun copy(): MediaHostState {
             val mediaHostState = MediaHostStateHolder()
             mediaHostState.expansion = expansion
@@ -312,15 +332,13 @@
 }
 
 /**
- * A description of a media host state that describes the behavior whenever the media carousel
- * is hosted. The HostState notifies the media players of changes to their properties, who
- * in turn will create view states from it.
- * When adding a new property to this, make sure to update the listener and notify them
- * about the changes.
- * In case you need to have a different rendering based on the state, you can add a new
- * constraintState to the [MediaViewController]. Otherwise, similar host states will resolve
- * to the same viewstate, a behavior that is described in [CacheKey]. Make sure to only update
- * that key if the underlying view needs to have a different measurement.
+ * A description of a media host state that describes the behavior whenever the media carousel is
+ * hosted. The HostState notifies the media players of changes to their properties, who in turn will
+ * create view states from it. When adding a new property to this, make sure to update the listener
+ * and notify them about the changes. In case you need to have a different rendering based on the
+ * state, you can add a new constraintState to the [MediaViewController]. Otherwise, similar host
+ * states will resolve to the same viewstate, a behavior that is described in [CacheKey]. Make sure
+ * to only update that key if the underlying view needs to have a different measurement.
  */
 interface MediaHostState {
 
@@ -330,46 +348,36 @@
     }
 
     /**
-     * The last measurement input that this state was measured with. Infers width and height of
-     * the players.
+     * The last measurement input that this state was measured with. Infers width and height of the
+     * players.
      */
     var measurementInput: MeasurementInput?
 
     /**
-     * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions),
-     * [EXPANDED] for fully expanded (up to 5 actions).
+     * The expansion of the player, [COLLAPSED] for fully collapsed (up to 3 actions), [EXPANDED]
+     * for fully expanded (up to 5 actions).
      */
     var expansion: Float
 
-    /**
-     * Fraction of the height animation.
-     */
+    /** Fraction of the height animation. */
     var squishFraction: Float
 
-    /**
-     * Is this host only showing active media or is it showing all of them including resumption?
-     */
+    /** Is this host only showing active media or is it showing all of them including resumption? */
     var showsOnlyActiveMedia: Boolean
 
-    /**
-     * If the view should be VISIBLE or GONE.
-     */
+    /** If the view should be VISIBLE or GONE. */
     val visible: Boolean
 
-    /**
-     * Does this host need any falsing protection?
-     */
+    /** Does this host need any falsing protection? */
     var falsingProtectionNeeded: Boolean
 
     /**
      * The parameters how the view disappears from this location when going to a host that's not
-     * visible. If modified, make sure to set this value again on the host to ensure the values
-     * are propagated
+     * visible. If modified, make sure to set this value again on the host to ensure the values are
+     * propagated
      */
     var disappearParameters: DisappearParameters
 
-    /**
-     * Get a copy of this view state, deepcopying all appropriate members
-     */
+    /** Get a copy of this view state, deepcopying all appropriate members */
     fun copy(): MediaHostState
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt
new file mode 100644
index 0000000..ae3ce33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHostStatesManager.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+/**
+ * A class responsible for managing all media host states of the various host locations and
+ * coordinating the heights among different players. This class can be used to get the most up to
+ * date state for any location.
+ */
+@SysUISingleton
+class MediaHostStatesManager @Inject constructor() {
+
+    private val callbacks: MutableSet<Callback> = mutableSetOf()
+    private val controllers: MutableSet<MediaViewController> = mutableSetOf()
+
+    /**
+     * The overall sizes of the carousel. This is needed to make sure all players in the carousel
+     * have equal size.
+     */
+    val carouselSizes: MutableMap<Int, MeasurementOutput> = mutableMapOf()
+
+    /** A map with all media states of all locations. */
+    val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
+
+    /**
+     * Notify that a media state for a given location has changed. Should only be called from Media
+     * hosts themselves.
+     */
+    fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) =
+        traceSection("MediaHostStatesManager#updateHostState") {
+            val currentState = mediaHostStates.get(location)
+            if (!hostState.equals(currentState)) {
+                val newState = hostState.copy()
+                mediaHostStates.put(location, newState)
+                updateCarouselDimensions(location, hostState)
+                // First update all the controllers to ensure they get the chance to measure
+                for (controller in controllers) {
+                    controller.stateCallback.onHostStateChanged(location, newState)
+                }
+
+                // Then update all other callbacks which may depend on the controllers above
+                for (callback in callbacks) {
+                    callback.onHostStateChanged(location, newState)
+                }
+            }
+        }
+
+    /**
+     * Get the dimensions of all players combined, which determines the overall height of the media
+     * carousel and the media hosts.
+     */
+    fun updateCarouselDimensions(
+        @MediaLocation location: Int,
+        hostState: MediaHostState
+    ): MeasurementOutput =
+        traceSection("MediaHostStatesManager#updateCarouselDimensions") {
+            val result = MeasurementOutput(0, 0)
+            for (controller in controllers) {
+                val measurement = controller.getMeasurementsForState(hostState)
+                measurement?.let {
+                    if (it.measuredHeight > result.measuredHeight) {
+                        result.measuredHeight = it.measuredHeight
+                    }
+                    if (it.measuredWidth > result.measuredWidth) {
+                        result.measuredWidth = it.measuredWidth
+                    }
+                }
+            }
+            carouselSizes[location] = result
+            return result
+        }
+
+    /** Add a callback to be called when a MediaState has updated */
+    fun addCallback(callback: Callback) {
+        callbacks.add(callback)
+    }
+
+    /** Remove a callback that listens to media states */
+    fun removeCallback(callback: Callback) {
+        callbacks.remove(callback)
+    }
+
+    /**
+     * Register a controller that listens to media states and is used to determine the size of the
+     * media carousel
+     */
+    fun addController(controller: MediaViewController) {
+        controllers.add(controller)
+    }
+
+    /** Notify the manager about the removal of a controller. */
+    fun removeController(controller: MediaViewController) {
+        controllers.remove(controller)
+    }
+
+    interface Callback {
+        /**
+         * Notify the callbacks that a media state for a host has changed, and that the
+         * corresponding view states should be updated and applied
+         */
+        fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
new file mode 100644
index 0000000..0e07465
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaScrollView.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.widget.HorizontalScrollView
+import com.android.systemui.Gefingerpoken
+import com.android.wm.shell.animation.physicsAnimator
+
+/**
+ * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when
+ * only measuring children but not the parent, when trying to apply a new scroll position
+ */
+class MediaScrollView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+    HorizontalScrollView(context, attrs, defStyleAttr) {
+
+    lateinit var contentContainer: ViewGroup
+        private set
+    var touchListener: Gefingerpoken? = null
+
+    /**
+     * The target value of the translation X animation. Only valid if the physicsAnimator is running
+     */
+    var animationTargetX = 0.0f
+
+    /**
+     * Get the current content translation. This is usually the normal translationX of the content,
+     * but when animating, it might differ
+     */
+    fun getContentTranslation() =
+        if (contentContainer.physicsAnimator.isRunning()) {
+            animationTargetX
+        } else {
+            contentContainer.translationX
+        }
+
+    /**
+     * Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
+     * carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX is
+     * always absolute. This function is its own inverse.
+     */
+    private fun transformScrollX(scrollX: Int): Int =
+        if (isLayoutRtl) {
+            contentContainer.width - width - scrollX
+        } else {
+            scrollX
+        }
+
+    /** Get the layoutDirection-relative (start-to-end) scroll X position of the carousel. */
+    var relativeScrollX: Int
+        get() = transformScrollX(scrollX)
+        set(value) {
+            scrollX = transformScrollX(value)
+        }
+
+    /** Allow all scrolls to go through, use base implementation */
+    override fun scrollTo(x: Int, y: Int) {
+        if (mScrollX != x || mScrollY != y) {
+            val oldX: Int = mScrollX
+            val oldY: Int = mScrollY
+            mScrollX = x
+            mScrollY = y
+            invalidateParentCaches()
+            onScrollChanged(mScrollX, mScrollY, oldX, oldY)
+            if (!awakenScrollBars()) {
+                postInvalidateOnAnimation()
+            }
+        }
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+        var intercept = false
+        touchListener?.let { intercept = it.onInterceptTouchEvent(ev) }
+        return super.onInterceptTouchEvent(ev) || intercept
+    }
+
+    override fun onTouchEvent(ev: MotionEvent?): Boolean {
+        var touch = false
+        touchListener?.let { touch = it.onTouchEvent(ev) }
+        return super.onTouchEvent(ev) || touch
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        contentContainer = getChildAt(0) as ViewGroup
+    }
+
+    override fun overScrollBy(
+        deltaX: Int,
+        deltaY: Int,
+        scrollX: Int,
+        scrollY: Int,
+        scrollRangeX: Int,
+        scrollRangeY: Int,
+        maxOverScrollX: Int,
+        maxOverScrollY: Int,
+        isTouchEvent: Boolean
+    ): Boolean {
+        if (getContentTranslation() != 0.0f) {
+            // When we're dismissing we ignore all the scrolling
+            return false
+        }
+        return super.overScrollBy(
+            deltaX,
+            deltaY,
+            scrollX,
+            scrollY,
+            scrollRangeX,
+            scrollRangeY,
+            maxOverScrollX,
+            maxOverScrollY,
+            isTouchEvent
+        )
+    }
+
+    /** Cancel the current touch event going on. */
+    fun cancelCurrentScroll() {
+        val now = SystemClock.uptimeMillis()
+        val event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
+        event.source = InputDevice.SOURCE_TOUCHSCREEN
+        super.onTouchEvent(event)
+        event.recycle()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
new file mode 100644
index 0000000..4bf3031
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.content.Context
+import android.content.res.Configuration
+import androidx.annotation.VisibleForTesting
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.R
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.calculateAlpha
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionLayoutController
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.traceSection
+import javax.inject.Inject
+
+/**
+ * A class responsible for controlling a single instance of a media player handling interactions
+ * with the view instance and keeping the media view states up to date.
+ */
+class MediaViewController
+@Inject
+constructor(
+    private val context: Context,
+    private val configurationController: ConfigurationController,
+    private val mediaHostStatesManager: MediaHostStatesManager,
+    private val logger: MediaViewLogger
+) {
+
+    /**
+     * Indicating that the media view controller is for a notification-based player, session-based
+     * player, or recommendation
+     */
+    enum class TYPE {
+        PLAYER,
+        RECOMMENDATION
+    }
+
+    companion object {
+        @JvmField val GUTS_ANIMATION_DURATION = 500L
+        val controlIds =
+            setOf(
+                R.id.media_progress_bar,
+                R.id.actionNext,
+                R.id.actionPrev,
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+                R.id.media_scrubbing_elapsed_time,
+                R.id.media_scrubbing_total_time
+            )
+
+        val detailIds =
+            setOf(
+                R.id.header_title,
+                R.id.header_artist,
+                R.id.actionPlayPause,
+            )
+    }
+
+    /** A listener when the current dimensions of the player change */
+    lateinit var sizeChangedListener: () -> Unit
+    private var firstRefresh: Boolean = true
+    @VisibleForTesting private var transitionLayout: TransitionLayout? = null
+    private val layoutController = TransitionLayoutController()
+    private var animationDelay: Long = 0
+    private var animationDuration: Long = 0
+    private var animateNextStateChange: Boolean = false
+    private val measurement = MeasurementOutput(0, 0)
+    private var type: TYPE = TYPE.PLAYER
+
+    /** A map containing all viewStates for all locations of this mediaState */
+    private val viewStates: MutableMap<CacheKey, TransitionViewState?> = mutableMapOf()
+
+    /**
+     * The ending location of the view where it ends when all animations and transitions have
+     * finished
+     */
+    @MediaLocation var currentEndLocation: Int = -1
+
+    /** The starting location of the view where it starts for all animations and transitions */
+    @MediaLocation private var currentStartLocation: Int = -1
+
+    /** The progress of the transition or 1.0 if there is no transition happening */
+    private var currentTransitionProgress: Float = 1.0f
+
+    /** A temporary state used to store intermediate measurements. */
+    private val tmpState = TransitionViewState()
+
+    /** A temporary state used to store intermediate measurements. */
+    private val tmpState2 = TransitionViewState()
+
+    /** A temporary state used to store intermediate measurements. */
+    private val tmpState3 = TransitionViewState()
+
+    /** A temporary cache key to be used to look up cache entries */
+    private val tmpKey = CacheKey()
+
+    /**
+     * The current width of the player. This might not factor in case the player is animating to the
+     * current state, but represents the end state
+     */
+    var currentWidth: Int = 0
+    /**
+     * The current height of the player. This might not factor in case the player is animating to
+     * the current state, but represents the end state
+     */
+    var currentHeight: Int = 0
+
+    /** Get the translationX of the layout */
+    var translationX: Float = 0.0f
+        private set
+        get() {
+            return transitionLayout?.translationX ?: 0.0f
+        }
+
+    /** Get the translationY of the layout */
+    var translationY: Float = 0.0f
+        private set
+        get() {
+            return transitionLayout?.translationY ?: 0.0f
+        }
+
+    /** A callback for RTL config changes */
+    private val configurationListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onConfigChanged(newConfig: Configuration?) {
+                // Because the TransitionLayout is not always attached (and calculates/caches layout
+                // results regardless of attach state), we have to force the layoutDirection of the
+                // view
+                // to the correct value for the user's current locale to ensure correct
+                // recalculation
+                // when/after calling refreshState()
+                newConfig?.apply {
+                    if (transitionLayout?.rawLayoutDirection != layoutDirection) {
+                        transitionLayout?.layoutDirection = layoutDirection
+                        refreshState()
+                    }
+                }
+            }
+        }
+
+    /** A callback for media state changes */
+    val stateCallback =
+        object : MediaHostStatesManager.Callback {
+            override fun onHostStateChanged(
+                @MediaLocation location: Int,
+                mediaHostState: MediaHostState
+            ) {
+                if (location == currentEndLocation || location == currentStartLocation) {
+                    setCurrentState(
+                        currentStartLocation,
+                        currentEndLocation,
+                        currentTransitionProgress,
+                        applyImmediately = false
+                    )
+                }
+            }
+        }
+
+    /**
+     * The expanded constraint set used to render a expanded player. If it is modified, make sure to
+     * call [refreshState]
+     */
+    val collapsedLayout = ConstraintSet()
+
+    /**
+     * The expanded constraint set used to render a collapsed player. If it is modified, make sure
+     * to call [refreshState]
+     */
+    val expandedLayout = ConstraintSet()
+
+    /** Whether the guts are visible for the associated player. */
+    var isGutsVisible = false
+        private set
+
+    init {
+        mediaHostStatesManager.addController(this)
+        layoutController.sizeChangedListener = { width: Int, height: Int ->
+            currentWidth = width
+            currentHeight = height
+            sizeChangedListener.invoke()
+        }
+        configurationController.addCallback(configurationListener)
+    }
+
+    /**
+     * Notify this controller that the view has been removed and all listeners should be destroyed
+     */
+    fun onDestroy() {
+        mediaHostStatesManager.removeController(this)
+        configurationController.removeCallback(configurationListener)
+    }
+
+    /** Show guts with an animated transition. */
+    fun openGuts() {
+        if (isGutsVisible) return
+        isGutsVisible = true
+        animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+        setCurrentState(
+            currentStartLocation,
+            currentEndLocation,
+            currentTransitionProgress,
+            applyImmediately = false
+        )
+    }
+
+    /**
+     * Close the guts for the associated player.
+     *
+     * @param immediate if `false`, it will animate the transition.
+     */
+    @JvmOverloads
+    fun closeGuts(immediate: Boolean = false) {
+        if (!isGutsVisible) return
+        isGutsVisible = false
+        if (!immediate) {
+            animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+        }
+        setCurrentState(
+            currentStartLocation,
+            currentEndLocation,
+            currentTransitionProgress,
+            applyImmediately = immediate
+        )
+    }
+
+    private fun ensureAllMeasurements() {
+        val mediaStates = mediaHostStatesManager.mediaHostStates
+        for (entry in mediaStates) {
+            obtainViewState(entry.value)
+        }
+    }
+
+    /** Get the constraintSet for a given expansion */
+    private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
+        if (expansion > 0) expandedLayout else collapsedLayout
+
+    /**
+     * Set the views to be showing/hidden based on the [isGutsVisible] for a given
+     * [TransitionViewState].
+     */
+    private fun setGutsViewState(viewState: TransitionViewState) {
+        val controlsIds =
+            when (type) {
+                TYPE.PLAYER -> MediaViewHolder.controlsIds
+                TYPE.RECOMMENDATION -> RecommendationViewHolder.controlsIds
+            }
+        val gutsIds = GutsViewHolder.ids
+        controlsIds.forEach { id ->
+            viewState.widgetStates.get(id)?.let { state ->
+                // Make sure to use the unmodified state if guts are not visible.
+                state.alpha = if (isGutsVisible) 0f else state.alpha
+                state.gone = if (isGutsVisible) true else state.gone
+            }
+        }
+        gutsIds.forEach { id ->
+            viewState.widgetStates.get(id)?.let { state ->
+                // Make sure to use the unmodified state if guts are visible
+                state.alpha = if (isGutsVisible) state.alpha else 0f
+                state.gone = if (isGutsVisible) state.gone else true
+            }
+        }
+    }
+
+    /** Apply squishFraction to a copy of viewState such that the cached version is untouched. */
+    internal fun squishViewState(
+        viewState: TransitionViewState,
+        squishFraction: Float
+    ): TransitionViewState {
+        val squishedViewState = viewState.copy()
+        squishedViewState.height = (squishedViewState.height * squishFraction).toInt()
+        controlIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, CONTROLS_DELAY, DURATION)
+            }
+        }
+
+        detailIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, DETAILS_DELAY, DURATION)
+            }
+        }
+
+        RecommendationViewHolder.mediaContainersIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, MEDIACONTAINERS_DELAY, DURATION)
+            }
+        }
+
+        RecommendationViewHolder.mediaTitlesAndSubtitlesIds.forEach { id ->
+            squishedViewState.widgetStates.get(id)?.let { state ->
+                state.alpha = calculateAlpha(squishFraction, MEDIATITLES_DELAY, DURATION)
+            }
+        }
+
+        return squishedViewState
+    }
+
+    /**
+     * Obtain a new viewState for a given media state. This usually returns a cached state, but if
+     * it's not available, it will recreate one by measuring, which may be expensive.
+     */
+    @VisibleForTesting
+    fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+        if (state == null || state.measurementInput == null) {
+            return null
+        }
+        // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
+        var cacheKey = getKey(state, isGutsVisible, tmpKey)
+        val viewState = viewStates[cacheKey]
+        if (viewState != null) {
+            // we already have cached this measurement, let's continue
+            if (state.squishFraction <= 1f) {
+                return squishViewState(viewState, state.squishFraction)
+            }
+            return viewState
+        }
+        // Copy the key since this might call recursively into it and we're using tmpKey
+        cacheKey = cacheKey.copy()
+        val result: TransitionViewState?
+
+        if (transitionLayout == null) {
+            return null
+        }
+        // Let's create a new measurement
+        if (state.expansion == 0.0f || state.expansion == 1.0f) {
+            result =
+                transitionLayout!!.calculateViewState(
+                    state.measurementInput!!,
+                    constraintSetForExpansion(state.expansion),
+                    TransitionViewState()
+                )
+
+            setGutsViewState(result)
+            // We don't want to cache interpolated or null states as this could quickly fill up
+            // our cache. We only cache the start and the end states since the interpolation
+            // is cheap
+            viewStates[cacheKey] = result
+        } else {
+            // This is an interpolated state
+            val startState = state.copy().also { it.expansion = 0.0f }
+
+            // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+            // from the start and end state and interpolate them
+            val startViewState = obtainViewState(startState) as TransitionViewState
+            val endState = state.copy().also { it.expansion = 1.0f }
+            val endViewState = obtainViewState(endState) as TransitionViewState
+            result =
+                layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
+        }
+        if (state.squishFraction <= 1f) {
+            return squishViewState(result, state.squishFraction)
+        }
+        return result
+    }
+
+    private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
+        result.apply {
+            heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
+            widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
+            expansion = state.expansion
+            gutsVisible = guts
+        }
+        return result
+    }
+
+    /**
+     * Attach a view to this controller. This may perform measurements if it's not available yet and
+     * should therefore be done carefully.
+     */
+    fun attach(transitionLayout: TransitionLayout, type: TYPE) =
+        traceSection("MediaViewController#attach") {
+            updateMediaViewControllerType(type)
+            logger.logMediaLocation("attach $type", currentStartLocation, currentEndLocation)
+            this.transitionLayout = transitionLayout
+            layoutController.attach(transitionLayout)
+            if (currentEndLocation == -1) {
+                return
+            }
+            // Set the previously set state immediately to the view, now that it's finally attached
+            setCurrentState(
+                startLocation = currentStartLocation,
+                endLocation = currentEndLocation,
+                transitionProgress = currentTransitionProgress,
+                applyImmediately = true
+            )
+        }
+
+    /**
+     * Obtain a measurement for a given location. This makes sure that the state is up to date and
+     * all widgets know their location. Calling this method may create a measurement if we don't
+     * have a cached value available already.
+     */
+    fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? =
+        traceSection("MediaViewController#getMeasurementsForState") {
+            val viewState = obtainViewState(hostState) ?: return null
+            measurement.measuredWidth = viewState.width
+            measurement.measuredHeight = viewState.height
+            return measurement
+        }
+
+    /**
+     * Set a new state for the controlled view which can be an interpolation between multiple
+     * locations.
+     */
+    fun setCurrentState(
+        @MediaLocation startLocation: Int,
+        @MediaLocation endLocation: Int,
+        transitionProgress: Float,
+        applyImmediately: Boolean
+    ) =
+        traceSection("MediaViewController#setCurrentState") {
+            currentEndLocation = endLocation
+            currentStartLocation = startLocation
+            currentTransitionProgress = transitionProgress
+            logger.logMediaLocation("setCurrentState", startLocation, endLocation)
+
+            val shouldAnimate = animateNextStateChange && !applyImmediately
+
+            val endHostState = mediaHostStatesManager.mediaHostStates[endLocation] ?: return
+            val startHostState = mediaHostStatesManager.mediaHostStates[startLocation]
+
+            // Obtain the view state that we'd want to be at the end
+            // The view might not be bound yet or has never been measured and in that case will be
+            // reset once the state is fully available
+            var endViewState = obtainViewState(endHostState) ?: return
+            endViewState = updateViewStateToCarouselSize(endViewState, endLocation, tmpState2)!!
+            layoutController.setMeasureState(endViewState)
+
+            // If the view isn't bound, we can drop the animation, otherwise we'll execute it
+            animateNextStateChange = false
+            if (transitionLayout == null) {
+                return
+            }
+
+            val result: TransitionViewState
+            var startViewState = obtainViewState(startHostState)
+            startViewState = updateViewStateToCarouselSize(startViewState, startLocation, tmpState3)
+
+            if (!endHostState.visible) {
+                // Let's handle the case where the end is gone first. In this case we take the
+                // start viewState and will make it gone
+                if (startViewState == null || startHostState == null || !startHostState.visible) {
+                    // the start isn't a valid state, let's use the endstate directly
+                    result = endViewState
+                } else {
+                    // Let's get the gone presentation from the start state
+                    result =
+                        layoutController.getGoneState(
+                            startViewState,
+                            startHostState.disappearParameters,
+                            transitionProgress,
+                            tmpState
+                        )
+                }
+            } else if (startHostState != null && !startHostState.visible) {
+                // We have a start state and it is gone.
+                // Let's get presentation from the endState
+                result =
+                    layoutController.getGoneState(
+                        endViewState,
+                        endHostState.disappearParameters,
+                        1.0f - transitionProgress,
+                        tmpState
+                    )
+            } else if (transitionProgress == 1.0f || startViewState == null) {
+                // We're at the end. Let's use that state
+                result = endViewState
+            } else if (transitionProgress == 0.0f) {
+                // We're at the start. Let's use that state
+                result = startViewState
+            } else {
+                result =
+                    layoutController.getInterpolatedState(
+                        startViewState,
+                        endViewState,
+                        transitionProgress,
+                        tmpState
+                    )
+            }
+            logger.logMediaSize("setCurrentState", result.width, result.height)
+            layoutController.setState(
+                result,
+                applyImmediately,
+                shouldAnimate,
+                animationDuration,
+                animationDelay
+            )
+        }
+
+    private fun updateViewStateToCarouselSize(
+        viewState: TransitionViewState?,
+        location: Int,
+        outState: TransitionViewState
+    ): TransitionViewState? {
+        val result = viewState?.copy(outState) ?: return null
+        val overrideSize = mediaHostStatesManager.carouselSizes[location]
+        overrideSize?.let {
+            // To be safe we're using a maximum here. The override size should always be set
+            // properly though.
+            result.height = Math.max(it.measuredHeight, result.height)
+            result.width = Math.max(it.measuredWidth, result.width)
+        }
+        logger.logMediaSize("update to carousel", result.width, result.height)
+        return result
+    }
+
+    private fun updateMediaViewControllerType(type: TYPE) {
+        this.type = type
+
+        // These XML resources contain ConstraintSets that will apply to this player type's layout
+        when (type) {
+            TYPE.PLAYER -> {
+                collapsedLayout.load(context, R.xml.media_session_collapsed)
+                expandedLayout.load(context, R.xml.media_session_expanded)
+            }
+            TYPE.RECOMMENDATION -> {
+                collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
+                expandedLayout.load(context, R.xml.media_recommendation_expanded)
+            }
+        }
+        refreshState()
+    }
+
+    /**
+     * Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation]. In the event
+     * of [location] not being visible, [locationWhenHidden] will be used instead.
+     *
+     * @param location Target
+     * @param locationWhenHidden Location that will be used when the target is not
+     * [MediaHost.visible]
+     * @return State require for executing a transition, and also the respective [MediaHost].
+     */
+    private fun obtainViewStateForLocation(@MediaLocation location: Int): TransitionViewState? {
+        val mediaHostState = mediaHostStatesManager.mediaHostStates[location] ?: return null
+        return obtainViewState(mediaHostState)
+    }
+
+    /**
+     * Notify that the location is changing right now and a [setCurrentState] change is imminent.
+     * This updates the width the view will me measured with.
+     */
+    fun onLocationPreChange(@MediaLocation newLocation: Int) {
+        obtainViewStateForLocation(newLocation)?.let { layoutController.setMeasureState(it) }
+    }
+
+    /** Request that the next state change should be animated with the given parameters. */
+    fun animatePendingStateChange(duration: Long, delay: Long) {
+        animateNextStateChange = true
+        animationDuration = duration
+        animationDelay = delay
+    }
+
+    /** Clear all existing measurements and refresh the state to match the view. */
+    fun refreshState() =
+        traceSection("MediaViewController#refreshState") {
+            // Let's clear all of our measurements and recreate them!
+            viewStates.clear()
+            if (firstRefresh) {
+                // This is the first bind, let's ensure we pre-cache all measurements. Otherwise
+                // We'll just load these on demand.
+                ensureAllMeasurements()
+                firstRefresh = false
+            }
+            setCurrentState(
+                currentStartLocation,
+                currentEndLocation,
+                currentTransitionProgress,
+                applyImmediately = true
+            )
+        }
+}
+
+/** An internal key for the cache of mediaViewStates. This is a subset of the full host state. */
+private data class CacheKey(
+    var widthMeasureSpec: Int = -1,
+    var heightMeasureSpec: Int = -1,
+    var expansion: Float = 0.0f,
+    var gutsVisible: Boolean = false
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
new file mode 100644
index 0000000..fdac33a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewLogger.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.dagger.MediaViewLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import javax.inject.Inject
+
+private const val TAG = "MediaView"
+
+/** A buffered log for media view events that are too noisy for regular logging */
+@SysUISingleton
+class MediaViewLogger @Inject constructor(@MediaViewLog private val buffer: LogBuffer) {
+    fun logMediaSize(reason: String, width: Int, height: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = reason
+                int1 = width
+                int2 = height
+            },
+            { "size ($str1): $int1 x $int2" }
+        )
+    }
+
+    fun logMediaLocation(reason: String, startLocation: Int, endLocation: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = reason
+                int1 = startLocation
+                int2 = endLocation
+            },
+            { "location ($str1): $int1 -> $int2" }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
index 48f4a16..1cdcf5e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MetadataAnimationHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MetadataAnimationHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
@@ -73,4 +73,4 @@
         exitAnimator.addListener(this)
         enterAnimator.addListener(this)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
similarity index 73%
rename from packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
index 6bc94cd..e9b2cf2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/SquigglyProgress.kt
@@ -1,4 +1,20 @@
-package com.android.systemui.media
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
@@ -23,8 +39,7 @@
 private const val TAG = "Squiggly"
 
 private const val TWO_PI = (Math.PI * 2f).toFloat()
-@VisibleForTesting
-internal const val DISABLED_ALPHA = 77
+@VisibleForTesting internal const val DISABLED_ALPHA = 77
 
 class SquigglyProgress : Drawable() {
 
@@ -86,26 +101,29 @@
                 lastFrameTime = SystemClock.uptimeMillis()
             }
             heightAnimator?.cancel()
-            heightAnimator = ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
-                if (animate) {
-                    startDelay = 60
-                    duration = 800
-                    interpolator = Interpolators.EMPHASIZED_DECELERATE
-                } else {
-                    duration = 550
-                    interpolator = Interpolators.STANDARD_DECELERATE
-                }
-                addUpdateListener {
-                    heightFraction = it.animatedValue as Float
-                    invalidateSelf()
-                }
-                addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator?) {
-                        heightAnimator = null
+            heightAnimator =
+                ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
+                    if (animate) {
+                        startDelay = 60
+                        duration = 800
+                        interpolator = Interpolators.EMPHASIZED_DECELERATE
+                    } else {
+                        duration = 550
+                        interpolator = Interpolators.STANDARD_DECELERATE
                     }
-                })
-                start()
-            }
+                    addUpdateListener {
+                        heightFraction = it.animatedValue as Float
+                        invalidateSelf()
+                    }
+                    addListener(
+                        object : AnimatorListenerAdapter() {
+                            override fun onAnimationEnd(animation: Animator?) {
+                                heightAnimator = null
+                            }
+                        }
+                    )
+                    start()
+                }
         }
 
     override fun draw(canvas: Canvas) {
@@ -120,9 +138,15 @@
         val progress = level / 10_000f
         val totalWidth = bounds.width().toFloat()
         val totalProgressPx = totalWidth * progress
-        val waveProgressPx = totalWidth * (
-            if (!transitionEnabled || progress > matchedWaveEndpoint) progress else
-            lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress)))
+        val waveProgressPx =
+            totalWidth *
+                (if (!transitionEnabled || progress > matchedWaveEndpoint) progress
+                else
+                    lerp(
+                        minWaveEndpoint,
+                        matchedWaveEndpoint,
+                        lerpInv(0f, matchedWaveEndpoint, progress)
+                    ))
 
         // Build Wiggly Path
         val waveStart = -phaseOffset - waveLength / 2f
@@ -132,10 +156,8 @@
         val computeAmplitude: (Float, Float) -> Float = { x, sign ->
             if (transitionEnabled) {
                 val length = transitionPeriods * waveLength
-                val coeff = lerpInvSat(
-                    waveProgressPx + length / 2f,
-                    waveProgressPx - length / 2f,
-                    x)
+                val coeff =
+                    lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x)
                 sign * heightFraction * lineAmplitude * coeff
             } else {
                 sign * heightFraction * lineAmplitude
@@ -156,10 +178,7 @@
             val nextX = currentX + dist
             val midX = currentX + dist / 2
             val nextAmp = computeAmplitude(nextX, waveSign)
-            path.cubicTo(
-                midX, currentAmp,
-                midX, nextAmp,
-                nextX, nextAmp)
+            path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp)
             currentAmp = nextAmp
             currentX = nextX
         }
@@ -229,7 +248,7 @@
 
     private fun updateColors(tintColor: Int, alpha: Int) {
         wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha)
-        linePaint.color = ColorUtils.setAlphaComponent(tintColor,
-                (DISABLED_ALPHA * (alpha / 255f)).toInt())
+        linePaint.color =
+            ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt())
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java
index ed3e109..6caf5c2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControllerFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControllerFactory.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.util;
 
 import android.annotation.NonNull;
 import android.content.Context;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
index b8185b9..bcfceaa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaDataUtils.java
@@ -14,15 +14,25 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.util;
 
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.text.TextUtils;
 
+/**
+ * Utility class with common methods for media controls
+ */
 public class MediaDataUtils {
 
+    /**
+     * Get the application label for a given package
+     * @param context the context to use
+     * @param packageName Package to check
+     * @param unknownName Fallback string if application is not found
+     * @return The label or fallback string
+     */
     public static String getAppLabel(Context context, String packageName, String unknownName) {
         if (TextUtils.isEmpty(packageName)) {
             return null;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt
index 75eb33d..91dac6f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaFeatureFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFeatureFlag.kt
@@ -14,15 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.util
 
 import android.content.Context
 import com.android.systemui.util.Utils
 import javax.inject.Inject
 
-/**
- * Provides access to the current value of the feature flag.
- */
+/** Provides access to the current value of the feature flag. */
 class MediaFeatureFlag @Inject constructor(private val context: Context) {
     val enabled
         get() = Utils.useQsMediaPlayer(context)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index b85ae48..8d4931a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.util
 
 import android.app.StatusBarManager
 import android.os.UserHandle
@@ -34,9 +34,7 @@
         return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
     }
 
-    /**
-     * Check whether we support displaying information about mute await connections.
-     */
+    /** Check whether we support displaying information about mute await connections. */
     fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT)
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
similarity index 66%
rename from packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
index 0baf01e..3ad8c21 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.util
 
 import com.android.internal.logging.InstanceId
 import com.android.internal.logging.InstanceIdSequence
@@ -22,22 +22,21 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaLocation
 import java.lang.IllegalArgumentException
 import javax.inject.Inject
 
 private const val INSTANCE_ID_MAX = 1 shl 20
 
-/**
- * A helper class to log events related to the media controls
- */
+/** A helper class to log events related to the media controls */
 @SysUISingleton
 class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) {
 
     private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX)
 
-    /**
-     * Get a new instance ID for a new media control
-     */
+    /** Get a new instance ID for a new media control */
     fun getNewInstanceId(): InstanceId {
         return instanceIdSequence.newInstanceId()
     }
@@ -48,12 +47,13 @@
         instanceId: InstanceId,
         playbackLocation: Int
     ) {
-        val event = when (playbackLocation) {
-            MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
-            MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
-            MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
-            else -> throw IllegalArgumentException("Unknown playback location")
-        }
+        val event =
+            when (playbackLocation) {
+                MediaData.PLAYBACK_LOCAL -> MediaUiEvent.LOCAL_MEDIA_ADDED
+                MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.CAST_MEDIA_ADDED
+                MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.REMOTE_MEDIA_ADDED
+                else -> throw IllegalArgumentException("Unknown playback location")
+            }
         logger.logWithInstanceId(event, uid, packageName, instanceId)
     }
 
@@ -63,12 +63,13 @@
         instanceId: InstanceId,
         playbackLocation: Int
     ) {
-        val event = when (playbackLocation) {
-            MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
-            MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
-            MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
-            else -> throw IllegalArgumentException("Unknown playback location")
-        }
+        val event =
+            when (playbackLocation) {
+                MediaData.PLAYBACK_LOCAL -> MediaUiEvent.TRANSFER_TO_LOCAL
+                MediaData.PLAYBACK_CAST_LOCAL -> MediaUiEvent.TRANSFER_TO_CAST
+                MediaData.PLAYBACK_CAST_REMOTE -> MediaUiEvent.TRANSFER_TO_REMOTE
+                else -> throw IllegalArgumentException("Unknown playback location")
+            }
         logger.logWithInstanceId(event, uid, packageName, instanceId)
     }
 
@@ -107,8 +108,12 @@
     }
 
     fun logLongPressSettings(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.OPEN_SETTINGS_LONG_PRESS, uid, packageName,
-            instanceId)
+        logger.logWithInstanceId(
+            MediaUiEvent.OPEN_SETTINGS_LONG_PRESS,
+            uid,
+            packageName,
+            instanceId
+        )
     }
 
     fun logCarouselSettings() {
@@ -117,12 +122,13 @@
     }
 
     fun logTapAction(buttonId: Int, uid: Int, packageName: String, instanceId: InstanceId) {
-        val event = when (buttonId) {
-            R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
-            R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
-            R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
-            else -> MediaUiEvent.TAP_ACTION_OTHER
-        }
+        val event =
+            when (buttonId) {
+                R.id.actionPlayPause -> MediaUiEvent.TAP_ACTION_PLAY_PAUSE
+                R.id.actionPrev -> MediaUiEvent.TAP_ACTION_PREV
+                R.id.actionNext -> MediaUiEvent.TAP_ACTION_NEXT
+                else -> MediaUiEvent.TAP_ACTION_OTHER
+            }
 
         logger.logWithInstanceId(event, uid, packageName, instanceId)
     }
@@ -140,148 +146,130 @@
     }
 
     fun logCarouselPosition(@MediaLocation location: Int) {
-        val event = when (location) {
-            MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
-            MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
-            MediaHierarchyManager.LOCATION_LOCKSCREEN ->
-                MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
-            MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
-                MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
-            else -> throw IllegalArgumentException("Unknown media carousel location $location")
-        }
+        val event =
+            when (location) {
+                MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
+                MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
+                MediaHierarchyManager.LOCATION_LOCKSCREEN ->
+                    MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
+                MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
+                    MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
+                else -> throw IllegalArgumentException("Unknown media carousel location $location")
+            }
         logger.log(event)
     }
 
     fun logRecommendationAdded(packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, packageName,
-            instanceId)
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_ADDED,
+            0,
+            packageName,
+            instanceId
+        )
     }
 
     fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, packageName,
-            instanceId)
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED,
+            0,
+            packageName,
+            instanceId
+        )
     }
 
     fun logRecommendationActivated(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED, uid, packageName,
-            instanceId)
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_ACTIVATED,
+            uid,
+            packageName,
+            instanceId
+        )
     }
 
     fun logRecommendationItemTap(packageName: String, instanceId: InstanceId, position: Int) {
-        logger.logWithInstanceIdAndPosition(MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP, 0,
-            packageName, instanceId, position)
+        logger.logWithInstanceIdAndPosition(
+            MediaUiEvent.MEDIA_RECOMMENDATION_ITEM_TAP,
+            0,
+            packageName,
+            instanceId,
+            position
+        )
     }
 
     fun logRecommendationCardTap(packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP, 0, packageName,
-            instanceId)
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_RECOMMENDATION_CARD_TAP,
+            0,
+            packageName,
+            instanceId
+        )
     }
 
     fun logOpenBroadcastDialog(uid: Int, packageName: String, instanceId: InstanceId) {
-        logger.logWithInstanceId(MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG, uid, packageName,
-            instanceId)
+        logger.logWithInstanceId(
+            MediaUiEvent.MEDIA_OPEN_BROADCAST_DIALOG,
+            uid,
+            packageName,
+            instanceId
+        )
     }
 }
 
 enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
     @UiEvent(doc = "A new media control was added for media playing locally on the device")
     LOCAL_MEDIA_ADDED(1029),
-
     @UiEvent(doc = "A new media control was added for media cast from the device")
     CAST_MEDIA_ADDED(1030),
-
     @UiEvent(doc = "A new media control was added for media playing remotely")
     REMOTE_MEDIA_ADDED(1031),
-
     @UiEvent(doc = "The media for an existing control was transferred to local playback")
     TRANSFER_TO_LOCAL(1032),
-
     @UiEvent(doc = "The media for an existing control was transferred to a cast device")
     TRANSFER_TO_CAST(1033),
-
     @UiEvent(doc = "The media for an existing control was transferred to a remote device")
     TRANSFER_TO_REMOTE(1034),
-
-    @UiEvent(doc = "A new resumable media control was added")
-    RESUME_MEDIA_ADDED(1013),
-
+    @UiEvent(doc = "A new resumable media control was added") RESUME_MEDIA_ADDED(1013),
     @UiEvent(doc = "An existing active media control was converted into resumable media")
     ACTIVE_TO_RESUME(1014),
-
-    @UiEvent(doc = "A media control timed out")
-    MEDIA_TIMEOUT(1015),
-
-    @UiEvent(doc = "A media control was removed from the carousel")
-    MEDIA_REMOVED(1016),
-
-    @UiEvent(doc = "User swiped to another control within the media carousel")
-    CAROUSEL_PAGE(1017),
-
-    @UiEvent(doc = "The user swiped away the media carousel")
-    DISMISS_SWIPE(1018),
-
-    @UiEvent(doc = "The user long pressed on a media control")
-    OPEN_LONG_PRESS(1019),
-
+    @UiEvent(doc = "A media control timed out") MEDIA_TIMEOUT(1015),
+    @UiEvent(doc = "A media control was removed from the carousel") MEDIA_REMOVED(1016),
+    @UiEvent(doc = "User swiped to another control within the media carousel") CAROUSEL_PAGE(1017),
+    @UiEvent(doc = "The user swiped away the media carousel") DISMISS_SWIPE(1018),
+    @UiEvent(doc = "The user long pressed on a media control") OPEN_LONG_PRESS(1019),
     @UiEvent(doc = "The user dismissed a media control via its long press menu")
     DISMISS_LONG_PRESS(1020),
-
     @UiEvent(doc = "The user opened media settings from a media control's long press menu")
     OPEN_SETTINGS_LONG_PRESS(1021),
-
     @UiEvent(doc = "The user opened media settings from the media carousel")
     OPEN_SETTINGS_CAROUSEL(1022),
-
     @UiEvent(doc = "The play/pause button on a media control was tapped")
     TAP_ACTION_PLAY_PAUSE(1023),
-
-    @UiEvent(doc = "The previous button on a media control was tapped")
-    TAP_ACTION_PREV(1024),
-
-    @UiEvent(doc = "The next button on a media control was tapped")
-    TAP_ACTION_NEXT(1025),
-
+    @UiEvent(doc = "The previous button on a media control was tapped") TAP_ACTION_PREV(1024),
+    @UiEvent(doc = "The next button on a media control was tapped") TAP_ACTION_NEXT(1025),
     @UiEvent(doc = "A custom or generic action button on a media control was tapped")
     TAP_ACTION_OTHER(1026),
-
-    @UiEvent(doc = "The user seeked on a media control using the seekbar")
-    ACTION_SEEK(1027),
-
+    @UiEvent(doc = "The user seeked on a media control using the seekbar") ACTION_SEEK(1027),
     @UiEvent(doc = "The user opened the output switcher from a media control")
     OPEN_OUTPUT_SWITCHER(1028),
-
-    @UiEvent(doc = "The user tapped on a media control view")
-    MEDIA_TAP_CONTENT_VIEW(1036),
-
-    @UiEvent(doc = "The media carousel moved to QQS")
-    MEDIA_CAROUSEL_LOCATION_QQS(1037),
-
-    @UiEvent(doc = "THe media carousel moved to QS")
-    MEDIA_CAROUSEL_LOCATION_QS(1038),
-
+    @UiEvent(doc = "The user tapped on a media control view") MEDIA_TAP_CONTENT_VIEW(1036),
+    @UiEvent(doc = "The media carousel moved to QQS") MEDIA_CAROUSEL_LOCATION_QQS(1037),
+    @UiEvent(doc = "THe media carousel moved to QS") MEDIA_CAROUSEL_LOCATION_QS(1038),
     @UiEvent(doc = "The media carousel moved to the lockscreen")
     MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
-
     @UiEvent(doc = "The media carousel moved to the dream state")
     MEDIA_CAROUSEL_LOCATION_DREAM(1040),
-
     @UiEvent(doc = "A media recommendation card was added to the media carousel")
     MEDIA_RECOMMENDATION_ADDED(1041),
-
     @UiEvent(doc = "A media recommendation card was removed from the media carousel")
     MEDIA_RECOMMENDATION_REMOVED(1042),
-
     @UiEvent(doc = "An existing media control was made active as a recommendation")
     MEDIA_RECOMMENDATION_ACTIVATED(1043),
-
     @UiEvent(doc = "User tapped on an item in a media recommendation card")
     MEDIA_RECOMMENDATION_ITEM_TAP(1044),
-
     @UiEvent(doc = "User tapped on a media recommendation card")
     MEDIA_RECOMMENDATION_CARD_TAP(1045),
-
     @UiEvent(doc = "User opened the broadcast dialog from a media control")
     MEDIA_OPEN_BROADCAST_DIALOG(1079);
 
     override fun getId() = metricId
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/media/SmallHash.java
rename to packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java
index de7aac6..97483a6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SmallHash.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/SmallHash.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.util;
 
 import java.util.Objects;
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index e15e2d3..3e5d337 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -19,11 +19,11 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.log.dagger.MediaTttReceiverLogBuffer;
 import com.android.systemui.log.dagger.MediaTttSenderLogBuffer;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaFlags;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostStatesManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostStatesManager;
+import com.android.systemui.media.controls.util.MediaFlags;
 import com.android.systemui.media.dream.dagger.MediaComplicationComponent;
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java
index 65c5bc7..69b5698 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaComplicationViewController.java
@@ -21,9 +21,9 @@
 
 import android.widget.FrameLayout;
 
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostState;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostState;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index 91e7b49..20e8ae6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -27,9 +27,9 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.DreamMediaEntryComplication;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaData;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.SmartspaceMediaData;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 
 import javax.inject.Inject;
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
index ffcc1f7..e260894 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
@@ -21,7 +21,7 @@
 import com.android.settingslib.media.LocalMediaManager
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.MediaFlags
+import com.android.systemui.media.controls.util.MediaFlags
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index 38c971e..120f7d6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -43,6 +43,21 @@
         )
     }
 
+    /**
+     * Logs an error in trying to update to [displayState].
+     *
+     * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or
+     * a [android.app.StatusBarManager.MediaTransferReceiverState].
+     */
+    fun logStateChangeError(displayState: Int) {
+        buffer.log(
+            tag,
+            LogLevel.ERROR,
+            { int1 = displayState },
+            { "Cannot display state=$int1; aborting" }
+        )
+    }
+
     /** Logs that we couldn't find information for [packageName]. */
     fun logPackageNotFound(packageName: String) {
         buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
index c3de94f..0a60437 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt
@@ -21,6 +21,8 @@
 import android.graphics.drawable.Drawable
 import com.android.settingslib.Utils
 import com.android.systemui.R
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 
 /** Utility methods for media tap-to-transfer. */
 class MediaTttUtils {
@@ -31,6 +33,23 @@
         const val WAKE_REASON = "MEDIA_TRANSFER_ACTIVATED"
 
         /**
+         * Returns the information needed to display the icon in [Icon] form.
+         *
+         * See [getIconInfoFromPackageName].
+         */
+        fun getIconFromPackageName(
+            context: Context,
+            appPackageName: String?,
+            logger: MediaTttLogger,
+        ): Icon {
+            val iconInfo = getIconInfoFromPackageName(context, appPackageName, logger)
+            return Icon.Loaded(
+                iconInfo.drawable,
+                ContentDescription.Loaded(iconInfo.contentDescription)
+            )
+        }
+
+        /**
          * Returns the information needed to display the icon.
          *
          * The information will either contain app name and icon of the app playing media, or a
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 089625c..dc794e6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -25,7 +25,6 @@
 import android.media.MediaRoute2Info
 import android.os.Handler
 import android.os.PowerManager
-import android.util.Log
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
@@ -116,7 +115,7 @@
         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
-            Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState")
+            logger.logStateChangeError(displayState)
             return
         }
         uiEventLogger.logReceiverStateChange(chipState)
@@ -236,5 +235,3 @@
 ) : TemporaryViewInfo {
     override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS
 }
-
-private const val RECEIVER_TAG = "MediaTapToTransferRcvr"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index c24b030..6e596ee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -18,17 +18,12 @@
 
 import android.app.StatusBarManager
 import android.content.Context
-import android.media.MediaRoute2Info
 import android.util.Log
-import android.view.View
 import androidx.annotation.StringRes
 import com.android.internal.logging.UiEventLogger
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
-import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
-import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 
 /**
  * A class enumerating all the possible states of the media tap-to-transfer chip on the sender
@@ -38,6 +33,7 @@
  * @property stringResId the res ID of the string that should be displayed in the chip. Null if the
  *   state should not have the chip be displayed.
  * @property transferStatus the transfer status that the chip state represents.
+ * @property endItem the item that should be displayed in the end section of the chip.
  * @property timeout the amount of time this chip should display on the screen before it times out
  *   and disappears.
  */
@@ -46,6 +42,7 @@
     val uiEvent: UiEventLogger.UiEventEnum,
     @StringRes val stringResId: Int?,
     val transferStatus: TransferStatus,
+    val endItem: SenderEndItem?,
     val timeout: Long = DEFAULT_TIMEOUT_MILLIS
 ) {
     /**
@@ -58,6 +55,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST,
         R.string.media_move_closer_to_start_cast,
         transferStatus = TransferStatus.NOT_STARTED,
+        endItem = null,
     ),
 
     /**
@@ -71,6 +69,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST,
         R.string.media_move_closer_to_end_cast,
         transferStatus = TransferStatus.NOT_STARTED,
+        endItem = null,
     ),
 
     /**
@@ -82,6 +81,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED,
         R.string.media_transfer_playing_different_device,
         transferStatus = TransferStatus.IN_PROGRESS,
+        endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -94,6 +94,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED,
         R.string.media_transfer_playing_this_device,
         transferStatus = TransferStatus.IN_PROGRESS,
+        endItem = SenderEndItem.Loading,
         timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS
     ),
 
@@ -105,36 +106,13 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED,
         R.string.media_transfer_playing_different_device,
         transferStatus = TransferStatus.SUCCEEDED,
-    ) {
-        override fun undoClickListener(
-            chipbarCoordinator: ChipbarCoordinator,
-            routeInfo: MediaRoute2Info,
-            undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger,
-            falsingManager: FalsingManager,
-        ): View.OnClickListener? {
-            if (undoCallback == null) {
-                return null
-            }
-            return View.OnClickListener {
-                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
-
-                uiEventLogger.logUndoClicked(
-                    MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
-                )
-                undoCallback.onUndoTriggered()
-                // The external service should eventually send us a TransferToThisDeviceTriggered
-                // state, but that may take too long to go through the binder and the user may be
-                // confused as to why the UI hasn't changed yet. So, we immediately change the UI
-                // here.
-                chipbarCoordinator.displayView(
-                    ChipSenderInfo(
-                        TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback
-                    )
-                )
-            }
-        }
-    },
+        endItem = SenderEndItem.UndoButton(
+            uiEventOnClick =
+            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED,
+            newState =
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED
+        ),
+    ),
 
     /**
      * A state representing that a transfer back to this device has been successfully completed.
@@ -144,36 +122,13 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
         R.string.media_transfer_playing_this_device,
         transferStatus = TransferStatus.SUCCEEDED,
-    ) {
-        override fun undoClickListener(
-            chipbarCoordinator: ChipbarCoordinator,
-            routeInfo: MediaRoute2Info,
-            undoCallback: IUndoMediaTransferCallback?,
-            uiEventLogger: MediaTttSenderUiEventLogger,
-            falsingManager: FalsingManager,
-        ): View.OnClickListener? {
-            if (undoCallback == null) {
-                return null
-            }
-            return View.OnClickListener {
-                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
-
-                uiEventLogger.logUndoClicked(
-                    MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
-                )
-                undoCallback.onUndoTriggered()
-                // The external service should eventually send us a TransferToReceiverTriggered
-                // state, but that may take too long to go through the binder and the user may be
-                // confused as to why the UI hasn't changed yet. So, we immediately change the UI
-                // here.
-                chipbarCoordinator.displayView(
-                    ChipSenderInfo(
-                        TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback
-                    )
-                )
-            }
-        }
-    },
+        endItem = SenderEndItem.UndoButton(
+            uiEventOnClick =
+            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED,
+            newState =
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED
+        ),
+    ),
 
     /** A state representing that a transfer to the receiver device has failed. */
     TRANSFER_TO_RECEIVER_FAILED(
@@ -181,6 +136,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED,
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
+        endItem = SenderEndItem.Error,
     ),
 
     /** A state representing that a transfer back to this device has failed. */
@@ -189,6 +145,7 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED,
         R.string.media_transfer_failed,
         transferStatus = TransferStatus.FAILED,
+        endItem = SenderEndItem.Error,
     ),
 
     /** A state representing that this device is far away from any receiver device. */
@@ -197,37 +154,27 @@
         MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER,
         stringResId = null,
         transferStatus = TransferStatus.TOO_FAR,
-    );
+        // We shouldn't be displaying the chipbar anyway
+        endItem = null,
+    ) {
+        override fun getChipTextString(context: Context, otherDeviceName: String): Text {
+            // TODO(b/245610654): Better way to handle this.
+            throw IllegalArgumentException("FAR_FROM_RECEIVER should never be displayed, " +
+                "so its string should never be fetched")
+        }
+    };
 
     /**
      * Returns a fully-formed string with the text that the chip should display.
      *
+     * Throws an NPE if [stringResId] is null.
+     *
      * @param otherDeviceName the name of the other device involved in the transfer.
      */
-    fun getChipTextString(context: Context, otherDeviceName: String): String? {
-        if (stringResId == null) {
-            return null
-        }
-        return context.getString(stringResId, otherDeviceName)
+    open fun getChipTextString(context: Context, otherDeviceName: String): Text {
+        return Text.Loaded(context.getString(stringResId!!, otherDeviceName))
     }
 
-    /**
-     * Returns a click listener for the undo button on the chip. Returns null if this chip state
-     * doesn't have an undo button.
-     *
-     * @param chipbarCoordinator passed as a parameter in case we want to display a new chipbar
-     *   when undo is clicked.
-     * @param undoCallback if present, the callback that should be called when the user clicks the
-     *   undo button. The undo button will only be shown if this is non-null.
-     */
-    open fun undoClickListener(
-        chipbarCoordinator: ChipbarCoordinator,
-        routeInfo: MediaRoute2Info,
-        undoCallback: IUndoMediaTransferCallback?,
-        uiEventLogger: MediaTttSenderUiEventLogger,
-        falsingManager: FalsingManager,
-    ): View.OnClickListener? = null
-
     companion object {
         /**
          * Returns the sender state enum associated with the given [displayState] from
@@ -253,6 +200,26 @@
     }
 }
 
+/** Represents the item that should be displayed in the end section of the chip. */
+sealed class SenderEndItem {
+    /** A loading icon should be displayed. */
+    object Loading : SenderEndItem()
+
+    /** An error icon should be displayed. */
+    object Error : SenderEndItem()
+
+    /**
+     * An undo button should be displayed.
+     *
+     * @property uiEventOnClick the UI event to log when this button is clicked.
+     * @property newState the state that should immediately be transitioned to.
+     */
+    data class UndoButton(
+        val uiEventOnClick: UiEventLogger.UiEventEnum,
+        @StatusBarManager.MediaTransferSenderState val newState: Int,
+    ) : SenderEndItem()
+}
+
 // Give the Transfer*Triggered states a longer timeout since those states represent an active
 // process and we should keep the user informed about it as long as possible (but don't allow it to
 // continue indefinitely).
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index 224303a..1fa8fae 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -19,16 +19,20 @@
 import android.app.StatusBarManager
 import android.content.Context
 import android.media.MediaRoute2Info
-import android.util.Log
+import android.view.View
+import com.android.internal.logging.UiEventLogger
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.media.taptotransfer.common.MediaTttUtils
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
-import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG
+import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
 import javax.inject.Inject
 
 /**
@@ -80,7 +84,7 @@
         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
-            Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState")
+            logger.logStateChangeError(displayState)
             return
         }
         uiEventLogger.logSenderStateChange(chipState)
@@ -107,7 +111,90 @@
             chipbarCoordinator.removeView(removalReason)
         } else {
             displayedState = chipState
-            chipbarCoordinator.displayView(ChipSenderInfo(chipState, routeInfo, undoCallback))
+            chipbarCoordinator.displayView(
+                createChipbarInfo(
+                    chipState,
+                    routeInfo,
+                    undoCallback,
+                    context,
+                    logger,
+                )
+            )
         }
     }
+
+    /**
+     * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
+     */
+    private fun createChipbarInfo(
+        chipStateSender: ChipStateSender,
+        routeInfo: MediaRoute2Info,
+        undoCallback: IUndoMediaTransferCallback?,
+        context: Context,
+        logger: MediaTttLogger,
+    ): ChipbarInfo {
+        val packageName = routeInfo.clientPackageName
+        val otherDeviceName = routeInfo.name.toString()
+
+        return ChipbarInfo(
+            // Display the app's icon as the start icon
+            startIcon = MediaTttUtils.getIconFromPackageName(context, packageName, logger),
+            text = chipStateSender.getChipTextString(context, otherDeviceName),
+            endItem =
+                when (chipStateSender.endItem) {
+                    null -> null
+                    is SenderEndItem.Loading -> ChipbarEndItem.Loading
+                    is SenderEndItem.Error -> ChipbarEndItem.Error
+                    is SenderEndItem.UndoButton -> {
+                        if (undoCallback != null) {
+                            getUndoButton(
+                                undoCallback,
+                                chipStateSender.endItem.uiEventOnClick,
+                                chipStateSender.endItem.newState,
+                                routeInfo,
+                            )
+                        } else {
+                            null
+                        }
+                    }
+                },
+            vibrationEffect = chipStateSender.transferStatus.vibrationEffect,
+        )
+    }
+
+    /**
+     * Returns an undo button for the chip.
+     *
+     * When the button is clicked: [undoCallback] will be triggered, [uiEvent] will be logged, and
+     * this coordinator will transition to [newState].
+     */
+    private fun getUndoButton(
+        undoCallback: IUndoMediaTransferCallback,
+        uiEvent: UiEventLogger.UiEventEnum,
+        @StatusBarManager.MediaTransferSenderState newState: Int,
+        routeInfo: MediaRoute2Info,
+    ): ChipbarEndItem.Button {
+        val onClickListener =
+            View.OnClickListener {
+                uiEventLogger.logUndoClicked(uiEvent)
+                undoCallback.onUndoTriggered()
+
+                // The external service should eventually send us a new TransferTriggered state, but
+                // but that may take too long to go through the binder and the user may be confused
+                // as to why the UI hasn't changed yet. So, we immediately change the UI here.
+                updateMediaTapToTransferSenderDisplay(
+                    newState,
+                    routeInfo,
+                    // Since we're force-updating the UI, we don't have any [undoCallback] from the
+                    // external service (and TransferTriggered states don't have undo callbacks
+                    // anyway).
+                    undoCallback = null,
+                )
+            }
+
+        return ChipbarEndItem.Button(
+            Text.Resource(R.string.media_transfer_undo),
+            onClickListener,
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
index f15720d..b963809 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/TransferStatus.kt
@@ -16,16 +16,36 @@
 
 package com.android.systemui.media.taptotransfer.sender
 
-/** Represents the different possible transfer states that we could be in. */
-enum class TransferStatus {
+import android.os.VibrationEffect
+
+/**
+ * Represents the different possible transfer states that we could be in and the vibration effects
+ * that come with updating transfer states.
+ *
+ * @property vibrationEffect an optional vibration effect when the transfer status is changed.
+ */
+enum class TransferStatus(
+    val vibrationEffect: VibrationEffect? = null,
+) {
     /** The transfer hasn't started yet. */
-    NOT_STARTED,
+    NOT_STARTED(
+        vibrationEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1.0f, 0)
+                .compose()
+    ),
     /** The transfer is currently ongoing but hasn't completed yet. */
-    IN_PROGRESS,
+    IN_PROGRESS(
+        vibrationEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 0)
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.7f, 70)
+                .compose(),
+    ),
     /** The transfer has completed successfully. */
     SUCCEEDED,
     /** The transfer has completed with a failure. */
-    FAILED,
+    FAILED(vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)),
     /** The device is too far away to do a transfer. */
     TOO_FAR,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
index de34cd6..88b8676 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/AppOpsPrivacyItemMonitor.kt
@@ -56,7 +56,8 @@
         val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA,
                 AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO,
                 AppOpsManager.OP_PHONE_CALL_MICROPHONE,
-                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO)
+                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+                AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO)
         val OPS_LOCATION = intArrayOf(
                 AppOpsManager.OP_COARSE_LOCATION,
                 AppOpsManager.OP_FINE_LOCATION)
@@ -210,6 +211,7 @@
             AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
             AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+            AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
             AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
             else -> return null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 482a139..bb2b441 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -52,6 +52,7 @@
 import com.android.systemui.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -98,10 +99,10 @@
     fun init()
 
     /**
-     * Show the foreground services dialog. The dialog will be expanded from [viewLaunchedFrom] if
+     * Show the foreground services dialog. The dialog will be expanded from [expandable] if
      * it's not `null`.
      */
-    fun showDialog(viewLaunchedFrom: View?)
+    fun showDialog(expandable: Expandable?)
 
     /** Add a [OnNumberOfPackagesChangedListener]. */
     fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
@@ -367,7 +368,7 @@
 
     override fun shouldUpdateFooterVisibility() = dialog == null
 
-    override fun showDialog(viewLaunchedFrom: View?) {
+    override fun showDialog(expandable: Expandable?) {
         synchronized(lock) {
             if (dialog == null) {
 
@@ -403,16 +404,18 @@
                 }
 
                 mainExecutor.execute {
-                    viewLaunchedFrom
-                        ?.let {
-                            dialogLaunchAnimator.showFromView(
-                                dialog, it,
-                                cuj = DialogCuj(
-                                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                                    INTERACTION_JANK_TAG
-                                )
+                    val controller =
+                        expandable?.dialogLaunchController(
+                            DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+                                INTERACTION_JANK_TAG,
                             )
-                        } ?: dialog.show()
+                        )
+                    if (controller != null) {
+                        dialogLaunchAnimator.show(dialog, controller)
+                    } else {
+                        dialog.show()
+                    }
                 }
 
                 backgroundExecutor.execute {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
index 9d64781..a9943e8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -32,6 +32,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -156,7 +157,7 @@
             startSettingsActivity()
         } else if (v === powerMenuLite) {
             uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
-            globalActionsDialog?.showOrHideDialog(false, true, v)
+            globalActionsDialog?.showOrHideDialog(false, true, Expandable.fromView(powerMenuLite))
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
index 7511278e..b1b9dd7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java
@@ -29,6 +29,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.qs.dagger.QSScope;
@@ -130,7 +131,7 @@
 
     @Override
     public void onClick(View view) {
-        mFgsManagerController.showDialog(mRootView);
+        mFgsManagerController.showDialog(Expandable.fromView(view));
     }
 
     public void refreshState() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 498a98b..920a108 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -49,7 +49,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index f6db775..abc0ade 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -29,9 +29,9 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
-import com.android.systemui.media.MediaHostState;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHostState;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 2727c83..2a80de0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -32,7 +32,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
 import com.android.systemui.qs.customize.QSCustomizerController;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
index 67bf300..6c1e956 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
@@ -39,6 +39,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.systemui.FontSizeUtils;
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.common.shared.model.Icon;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -169,7 +170,7 @@
 
     // TODO(b/242040009): Remove this.
     public void showDeviceMonitoringDialog() {
-        mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, mView);
+        mQSSecurityFooterUtils.showDeviceMonitoringDialog(mContext, Expandable.fromView(mView));
     }
 
     public void refreshState() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
index ae6ed20..67bc769 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java
@@ -75,6 +75,7 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.DialogCuj;
 import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.common.shared.model.ContentDescription;
 import com.android.systemui.common.shared.model.Icon;
 import com.android.systemui.dagger.SysUISingleton;
@@ -190,8 +191,9 @@
     }
 
     /** Show the device monitoring dialog. */
-    public void showDeviceMonitoringDialog(Context quickSettingsContext, @Nullable View view) {
-        createDialog(quickSettingsContext, view);
+    public void showDeviceMonitoringDialog(Context quickSettingsContext,
+            @Nullable Expandable expandable) {
+        createDialog(quickSettingsContext, expandable);
     }
 
     /**
@@ -440,7 +442,7 @@
         }
     }
 
-    private void createDialog(Context quickSettingsContext, @Nullable View view) {
+    private void createDialog(Context quickSettingsContext, @Nullable Expandable expandable) {
         mShouldUseSettingsButton.set(false);
         mBgHandler.post(() -> {
             String settingsButtonText = getSettingsButton();
@@ -453,9 +455,12 @@
                         ? settingsButtonText : getNegativeButton(), this);
 
                 mDialog.setView(dialogView);
-                if (view != null && view.isAggregatedVisible()) {
-                    mDialogLaunchAnimator.showFromView(mDialog, view, new DialogCuj(
-                            InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG));
+                DialogLaunchAnimator.Controller controller =
+                        expandable != null ? expandable.dialogLaunchController(new DialogCuj(
+                                InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG))
+                                : null;
+                if (controller != null) {
+                    mDialogLaunchAnimator.show(mDialog, controller);
                 } else {
                     mDialog.show();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index ac46c85..f37d668 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -34,10 +34,12 @@
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dumpable;
+import com.android.systemui.ProtoDumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -48,6 +50,7 @@
 import com.android.systemui.qs.external.TileServiceKey;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.nano.QsTileState;
 import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
@@ -59,16 +62,20 @@
 import com.android.systemui.util.leak.GarbageMonitor;
 import com.android.systemui.util.settings.SecureSettings;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
@@ -82,7 +89,7 @@
  * This class also provides the interface for adding/removing/changing tiles.
  */
 @SysUISingleton
-public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable {
+public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable {
     private static final String TAG = "QSTileHost";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final int MAX_QS_INSTANCE_ID = 1 << 20;
@@ -671,4 +678,15 @@
         mTiles.values().stream().filter(obj -> obj instanceof Dumpable)
                 .forEach(o -> ((Dumpable) o).dump(pw, args));
     }
+
+    @Override
+    public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) {
+        List<QsTileState> data = mTiles.values().stream()
+                .map(QSTile::getState)
+                .map(TileStateToProtoKt::toProto)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        systemUIProtoDump.tiles = data.toArray(new QsTileState[0]);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 9739974..6aabe3b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -26,8 +26,8 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaHierarchyManager;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSScope;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt
new file mode 100644
index 0000000..2c8a5a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileStateToProto.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.external.CustomTile
+import com.android.systemui.qs.nano.QsTileState
+import com.android.systemui.util.nano.ComponentNameProto
+
+fun QSTile.State.toProto(): QsTileState? {
+    if (TextUtils.isEmpty(spec)) return null
+    val state = QsTileState()
+    if (spec.startsWith(CustomTile.PREFIX)) {
+        val protoComponentName = ComponentNameProto()
+        val tileComponentName = CustomTile.getComponentFromSpec(spec)
+        protoComponentName.packageName = tileComponentName.packageName
+        protoComponentName.className = tileComponentName.className
+        state.componentName = protoComponentName
+    } else {
+        state.spec = spec
+    }
+    state.state =
+        when (this.state) {
+            Tile.STATE_UNAVAILABLE -> QsTileState.UNAVAILABLE
+            Tile.STATE_INACTIVE -> QsTileState.INACTIVE
+            Tile.STATE_ACTIVE -> QsTileState.ACTIVE
+            else -> QsTileState.UNAVAILABLE
+        }
+    label?.let { state.label = it.toString() }
+    secondaryLabel?.let { state.secondaryLabel = it.toString() }
+    if (this is QSTile.BooleanState) {
+        state.booleanState = value
+    }
+    return state
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
index cf9b41c..9ba3501 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractor.kt
@@ -23,13 +23,11 @@
 import android.content.IntentFilter
 import android.os.UserHandle
 import android.provider.Settings
-import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.logging.nano.MetricsProto
 import com.android.internal.util.FrameworkStatsLog
-import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
@@ -74,37 +72,27 @@
     val deviceMonitoringDialogRequests: Flow<Unit>
 
     /**
-     * Show the device monitoring dialog, expanded from [view].
-     *
-     * Important: [view] must be associated to the same [Context] as the [Quick Settings fragment]
-     * [com.android.systemui.qs.QSFragment].
-     */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showDeviceMonitoringDialog(view: View)
-
-    /**
-     * Show the device monitoring dialog.
+     * Show the device monitoring dialog, expanded from [expandable] if it's not null.
      *
      * Important: [quickSettingsContext] *must* be the [Context] associated to the [Quick Settings
      * fragment][com.android.systemui.qs.QSFragment].
      */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showDeviceMonitoringDialog(quickSettingsContext: Context)
+    fun showDeviceMonitoringDialog(quickSettingsContext: Context, expandable: Expandable?)
 
     /** Show the foreground services dialog. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showForegroundServicesDialog(view: View)
+    fun showForegroundServicesDialog(expandable: Expandable)
 
     /** Show the power menu dialog. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View)
+    fun showPowerMenuDialog(
+        globalActionsDialogLite: GlobalActionsDialogLite,
+        expandable: Expandable,
+    )
 
     /** Show the settings. */
     fun showSettings(expandable: Expandable)
 
     /** Show the user switcher. */
-    // TODO(b/230830644): Replace view by Expandable interface.
-    fun showUserSwitcher(view: View)
+    fun showUserSwitcher(context: Context, expandable: Expandable)
 }
 
 @SysUISingleton
@@ -147,28 +135,32 @@
             null,
         )
 
-    override fun showDeviceMonitoringDialog(view: View) {
-        qsSecurityFooterUtils.showDeviceMonitoringDialog(view.context, view)
-        DevicePolicyEventLogger.createEvent(
-                FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
-            )
-            .write()
+    override fun showDeviceMonitoringDialog(
+        quickSettingsContext: Context,
+        expandable: Expandable?,
+    ) {
+        qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+        if (expandable != null) {
+            DevicePolicyEventLogger.createEvent(
+                    FrameworkStatsLog.DEVICE_POLICY_EVENT__EVENT_ID__DO_USER_INFO_CLICKED
+                )
+                .write()
+        }
     }
 
-    override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
-        qsSecurityFooterUtils.showDeviceMonitoringDialog(quickSettingsContext, /* view= */ null)
+    override fun showForegroundServicesDialog(expandable: Expandable) {
+        fgsManagerController.showDialog(expandable)
     }
 
-    override fun showForegroundServicesDialog(view: View) {
-        fgsManagerController.showDialog(view)
-    }
-
-    override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
+    override fun showPowerMenuDialog(
+        globalActionsDialogLite: GlobalActionsDialogLite,
+        expandable: Expandable,
+    ) {
         uiEventLogger.log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
         globalActionsDialogLite.showOrHideDialog(
             /* keyguardShowing= */ false,
             /* isDeviceProvisioned= */ true,
-            view,
+            expandable,
         )
     }
 
@@ -189,21 +181,21 @@
         )
     }
 
-    override fun showUserSwitcher(view: View) {
+    override fun showUserSwitcher(context: Context, expandable: Expandable) {
         if (!featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) {
-            userSwitchDialogController.showDialog(view)
+            userSwitchDialogController.showDialog(context, expandable)
             return
         }
 
         val intent =
-            Intent(view.context, UserSwitcherActivity::class.java).apply {
+            Intent(context, UserSwitcherActivity::class.java).apply {
                 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
             }
 
         activityStarter.startActivity(
             intent,
             true /* dismissShade */,
-            ActivityLaunchAnimator.Controller.fromView(view, null),
+            expandable.activityLaunchController(),
             true /* showOverlockscreenwhenlocked */,
             UserHandle.SYSTEM,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index dd1ffcc..3e39c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -31,6 +31,7 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.R
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.people.ui.view.PeopleViewBinder.bind
@@ -125,7 +126,7 @@
                 launch {
                     viewModel.security.collect { security ->
                         if (previousSecurity != security) {
-                            bindSecurity(securityHolder, security)
+                            bindSecurity(view.context, securityHolder, security)
                             previousSecurity = security
                         }
                     }
@@ -159,6 +160,7 @@
     }
 
     private fun bindSecurity(
+        quickSettingsContext: Context,
         securityHolder: TextButtonViewHolder,
         security: FooterActionsSecurityButtonViewModel?,
     ) {
@@ -171,9 +173,12 @@
         // Make sure that the chevron is visible and that the button is clickable if there is a
         // listener.
         val chevron = securityHolder.chevron
-        if (security.onClick != null) {
+        val onClick = security.onClick
+        if (onClick != null) {
             securityView.isClickable = true
-            securityView.setOnClickListener(security.onClick)
+            securityView.setOnClickListener {
+                onClick(quickSettingsContext, Expandable.fromView(securityView))
+            }
             chevron.isVisible = true
         } else {
             securityView.isClickable = false
@@ -205,7 +210,9 @@
             foregroundServicesWithNumberView.isVisible = false
 
             foregroundServicesWithTextView.isVisible = true
-            foregroundServicesWithTextView.setOnClickListener(foregroundServices.onClick)
+            foregroundServicesWithTextView.setOnClickListener {
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+            }
             foregroundServicesWithTextHolder.text.text = foregroundServices.text
             foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
         } else {
@@ -213,7 +220,9 @@
             foregroundServicesWithTextView.isVisible = false
 
             foregroundServicesWithNumberView.visibility = View.VISIBLE
-            foregroundServicesWithNumberView.setOnClickListener(foregroundServices.onClick)
+            foregroundServicesWithNumberView.setOnClickListener {
+                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
+            }
             foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
             foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
             foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
@@ -229,7 +238,7 @@
         }
 
         buttonView.setBackgroundResource(model.background)
-        buttonView.setOnClickListener(model.onClick)
+        buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
 
         val icon = model.icon
         val iconView = button.icon
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 9b5f683..8d819da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.footer.ui.viewmodel
 
 import android.annotation.DrawableRes
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
 /**
@@ -29,7 +29,5 @@
     val icon: Icon,
     val iconTint: Int?,
     @DrawableRes val background: Int,
-    // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
-    // or activity.
-    val onClick: (View) -> Unit,
+    val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
index 98b53cb..ff8130d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsForegroundServicesButtonViewModel.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 
 /** A ViewModel for the foreground services button. */
 data class FooterActionsForegroundServicesButtonViewModel(
@@ -24,5 +24,5 @@
     val text: String,
     val displayText: Boolean,
     val hasNewChanges: Boolean,
-    val onClick: (View) -> Unit,
+    val onClick: (Expandable) -> Unit,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
index 98ab129..3450505 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsSecurityButtonViewModel.kt
@@ -16,12 +16,13 @@
 
 package com.android.systemui.qs.footer.ui.viewmodel
 
-import android.view.View
+import android.content.Context
+import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 
 /** A ViewModel for the security button. */
 data class FooterActionsSecurityButtonViewModel(
     val icon: Icon,
     val text: String,
-    val onClick: ((View) -> Unit)?,
+    val onClick: ((quickSettingsContext: Context, Expandable) -> Unit)?,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index d3c06f6..dee6fad 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.util.Log
-import android.view.View
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -199,50 +198,51 @@
      */
     suspend fun observeDeviceMonitoringDialogRequests(quickSettingsContext: Context) {
         footerActionsInteractor.deviceMonitoringDialogRequests.collect {
-            footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext)
+            footerActionsInteractor.showDeviceMonitoringDialog(
+                quickSettingsContext,
+                expandable = null,
+            )
         }
     }
 
-    private fun onSecurityButtonClicked(view: View) {
+    private fun onSecurityButtonClicked(quickSettingsContext: Context, expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showDeviceMonitoringDialog(view)
+        footerActionsInteractor.showDeviceMonitoringDialog(quickSettingsContext, expandable)
     }
 
-    private fun onForegroundServiceButtonClicked(view: View) {
+    private fun onForegroundServiceButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showForegroundServicesDialog(view)
+        footerActionsInteractor.showForegroundServicesDialog(expandable)
     }
 
-    private fun onUserSwitcherClicked(view: View) {
+    private fun onUserSwitcherClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showUserSwitcher(view)
+        footerActionsInteractor.showUserSwitcher(context, expandable)
     }
 
-    // TODO(b/230830644): Replace View by an Expandable interface that can expand in either dialog
-    // or activity.
-    private fun onSettingsButtonClicked(view: View) {
+    private fun onSettingsButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showSettings(Expandable.fromView(view))
+        footerActionsInteractor.showSettings(expandable)
     }
 
-    private fun onPowerButtonClicked(view: View) {
+    private fun onPowerButtonClicked(expandable: Expandable) {
         if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return
         }
 
-        footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, view)
+        footerActionsInteractor.showPowerMenuDialog(globalActionsDialogLite, expandable)
     }
 
     private fun userSwitcherButton(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
new file mode 100644
index 0000000..2a61033
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/proto/tiles.proto
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package com.android.systemui.qs;
+
+import "frameworks/base/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto";
+
+option java_multiple_files = true;
+
+message QsTileState {
+  oneof identifier {
+    string spec = 1;
+    com.android.systemui.util.ComponentNameProto component_name = 2;
+  }
+
+  enum State {
+    UNAVAILABLE = 0;
+    INACTIVE = 1;
+    ACTIVE = 2;
+  }
+
+  State state = 3;
+  oneof optional_boolean_state {
+    bool boolean_state = 4;
+  }
+  oneof optional_label {
+    string label = 5;
+  }
+  oneof optional_secondary_label {
+    string secondary_label = 6;
+  }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index b6b657e..57a00c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -204,6 +204,15 @@
             Trace.endSection();
         }
 
+        @Override
+        public void onUserListItemClicked(@NonNull UserRecord record,
+                @Nullable UserSwitchDialogController.DialogShower dialogShower) {
+            if (dialogShower != null) {
+                mDialogShower.dismiss();
+            }
+            super.onUserListItemClicked(record, dialogShower);
+        }
+
         public void linkToViewGroup(ViewGroup viewGroup) {
             PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index bdcc6b0..314252b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -23,13 +23,13 @@
 import android.content.Intent
 import android.provider.Settings
 import android.view.LayoutInflater
-import android.view.View
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.R
 import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -77,10 +77,10 @@
      * Show a [UserDialog].
      *
      * Populate the dialog with information from and adapter obtained from
-     * [userDetailViewAdapterProvider] and show it as launched from [view].
+     * [userDetailViewAdapterProvider] and show it as launched from [expandable].
      */
-    fun showDialog(view: View) {
-        with(dialogFactory(view.context)) {
+    fun showDialog(context: Context, expandable: Expandable) {
+        with(dialogFactory(context)) {
             setShowForAllUsers(true)
             setCanceledOnTouchOutside(true)
 
@@ -112,13 +112,19 @@
 
             adapter.linkToViewGroup(gridFrame.findViewById(R.id.grid))
 
-            dialogLaunchAnimator.showFromView(
-                this, view,
-                cuj = DialogCuj(
-                    InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
-                    INTERACTION_JANK_TAG
+            val controller =
+                expandable.dialogLaunchController(
+                    DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
                 )
-            )
+            if (controller != null) {
+                dialogLaunchAnimator.show(
+                    this,
+                    controller,
+                )
+            } else {
+                show()
+            }
+
             uiEventLogger.log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN)
             adapter.injectDialogShower(DialogShowerImpl(this, dialogLaunchAnimator))
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 231e415..d524a35 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -20,6 +20,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
+import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
@@ -634,6 +635,11 @@
                         return true;
                     }
                 });
+
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mScreenshotView.badgeScreenshot(
+                    mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
+        }
         mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
         if (DEBUG_WINDOW) {
             Log.d(TAG, "setContentView: " + mScreenshotView);
@@ -1038,7 +1044,7 @@
 
     private boolean isUserSetupComplete(UserHandle owner) {
         return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
-                        .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+                .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 26cbcbf..27331ae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -74,7 +74,6 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
@@ -122,15 +121,9 @@
     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
     private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
-    private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350;
-    private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350;
-    private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
-    private static final float ROUNDED_CORNER_RADIUS = .25f;
     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
 
-    private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
-
     private final Resources mResources;
     private final Interpolator mFastOutSlowIn;
     private final DisplayMetrics mDisplayMetrics;
@@ -145,6 +138,7 @@
     private ImageView mScrollingScrim;
     private DraggableConstraintLayout mScreenshotStatic;
     private ImageView mScreenshotPreview;
+    private ImageView mScreenshotBadge;
     private View mScreenshotPreviewBorder;
     private ImageView mScrollablePreview;
     private ImageView mScreenshotFlash;
@@ -355,6 +349,7 @@
         mScreenshotPreviewBorder = requireNonNull(
                 findViewById(R.id.screenshot_preview_border));
         mScreenshotPreview.setClipToOutline(true);
+        mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge));
 
         mActionsContainerBackground = requireNonNull(findViewById(
                 R.id.actions_container_background));
@@ -595,8 +590,11 @@
 
         ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
         borderFadeIn.setDuration(100);
-        borderFadeIn.addUpdateListener((animation) ->
-                mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction()));
+        borderFadeIn.addUpdateListener((animation) -> {
+            float borderAlpha = animation.getAnimatedFraction();
+            mScreenshotPreviewBorder.setAlpha(borderAlpha);
+            mScreenshotBadge.setAlpha(borderAlpha);
+        });
 
         if (showFlash) {
             dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
@@ -763,11 +761,18 @@
         return animator;
     }
 
+    void badgeScreenshot(Drawable badge) {
+        mScreenshotBadge.setImageDrawable(badge);
+        mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
+    }
+
     void setChipIntents(ScreenshotController.SavedImageData imageData) {
         mShareChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
-                mActionExecutor.launchIntentAsync(ActionIntentCreator.INSTANCE.createShareIntent(
+                prepareSharedTransition();
+                mActionExecutor.launchIntentAsync(
+                        ActionIntentCreator.INSTANCE.createShareIntent(
                                 imageData.uri, imageData.subject),
                         imageData.shareTransition.get().bundle,
                         imageData.owner.getIdentifier(), false);
@@ -778,6 +783,7 @@
         mEditChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+                prepareSharedTransition();
                 mActionExecutor.launchIntentAsync(
                         ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
                         imageData.editTransition.get().bundle,
@@ -789,6 +795,7 @@
         mScreenshotPreview.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+                prepareSharedTransition();
                 mActionExecutor.launchIntentAsync(
                         ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
                         imageData.editTransition.get().bundle,
@@ -1023,6 +1030,9 @@
         mScreenshotPreview.setVisibility(View.INVISIBLE);
         mScreenshotPreview.setAlpha(1f);
         mScreenshotPreviewBorder.setAlpha(0);
+        mScreenshotBadge.setAlpha(0f);
+        mScreenshotBadge.setVisibility(View.GONE);
+        mScreenshotBadge.setImageDrawable(null);
         mPendingSharedTransition = false;
         mActionsContainerBackground.setVisibility(View.GONE);
         mActionsContainer.setVisibility(View.GONE);
@@ -1064,6 +1074,12 @@
         }
     }
 
+    private void prepareSharedTransition() {
+        mPendingSharedTransition = true;
+        // fade out non-preview UI
+        createScreenshotFadeDismissAnimation().start();
+    }
+
     ValueAnimator createScreenshotFadeDismissAnimation() {
         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
         alphaAnim.addUpdateListener(animation -> {
@@ -1072,6 +1088,7 @@
             mActionsContainerBackground.setAlpha(alpha);
             mActionsContainer.setAlpha(alpha);
             mScreenshotPreviewBorder.setAlpha(alpha);
+            mScreenshotBadge.setAlpha(alpha);
         });
         alphaAnim.setDuration(600);
         return alphaAnim;
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 6e9f859..d5a3954 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -20,6 +20,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import android.app.Activity;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.Gravity;
@@ -36,6 +37,8 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 
+import java.util.List;
+
 import javax.inject.Inject;
 
 /** A dialog that provides controls for adjusting the screen brightness. */
@@ -83,6 +86,15 @@
         lp.leftMargin = horizontalMargin;
         lp.rightMargin = horizontalMargin;
         frame.setLayoutParams(lp);
+        Rect bounds = new Rect();
+        frame.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    // Exclude this view (and its horizontal margins) from triggering gestures.
+                    // This prevents back gesture from being triggered by dragging close to the
+                    // edge of the slider (0% or 100%).
+                    bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top);
+                    v.setSystemGestureExclusionRects(List.of(bounds));
+                });
 
         BrightnessSliderController controller = mToggleSliderFactory.create(this, frame);
         controller.init();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 8b0b9de..b39175e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -147,10 +147,12 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.media.KeyguardMediaController;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaHierarchyManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.FalsingManager.FalsingTapListener;
@@ -583,6 +585,7 @@
     private final SysUiState mSysUiState;
 
     private final NotificationShadeDepthController mDepthController;
+    private final NavigationBarController mNavigationBarController;
     private final int mDisplayId;
 
     private KeyguardIndicationController mKeyguardIndicationController;
@@ -861,6 +864,7 @@
             PrivacyDotViewController privacyDotViewController,
             TapAgainViewController tapAgainViewController,
             NavigationModeController navigationModeController,
+            NavigationBarController navigationBarController,
             FragmentService fragmentService,
             ContentResolver contentResolver,
             RecordingController recordingController,
@@ -954,6 +958,7 @@
         mNotificationsQSContainerController = notificationsQSContainerController;
         mNotificationListContainer = notificationListContainer;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
+        mNavigationBarController = navigationBarController;
         mKeyguardBottomAreaViewControllerProvider = keyguardBottomAreaViewControllerProvider;
         mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
@@ -1443,6 +1448,16 @@
         mMaxAllowedKeyguardNotifications = maxAllowed;
     }
 
+    @VisibleForTesting
+    boolean getClosing() {
+        return mClosing;
+    }
+
+    @VisibleForTesting
+    boolean getIsFlinging() {
+        return mIsFlinging;
+    }
+
     private void updateMaxDisplayedNotifications(boolean recompute) {
         if (recompute) {
             setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
@@ -1675,9 +1690,9 @@
                 transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
 
                 boolean customClockAnimation =
-                        mKeyguardStatusViewController
-                                .getClockAnimations()
-                                .getHasCustomPositionUpdatedAnimation();
+                            mKeyguardStatusViewController.getClockAnimations() != null
+                            && mKeyguardStatusViewController.getClockAnimations()
+                                    .getHasCustomPositionUpdatedAnimation();
 
                 if (mFeatureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION) && customClockAnimation) {
                     // Find the clock, so we can exclude it from this transition.
@@ -2671,12 +2686,16 @@
             mQsExpanded = expanded;
             updateQsState();
             updateExpandedHeightToMaxHeight();
-            mFalsingCollector.setQsExpanded(expanded);
-            mCentralSurfaces.setQsExpanded(expanded);
-            mNotificationsQSContainerController.setQsExpanded(expanded);
-            mPulseExpansionHandler.setQsExpanded(expanded);
-            mKeyguardBypassController.setQSExpanded(expanded);
-            mPrivacyDotViewController.setQsExpanded(expanded);
+            setStatusAccessibilityImportance(expanded
+                    ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                    : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            updateSystemUiStateFlags();
+            NavigationBarView navigationBarView =
+                    mNavigationBarController.getNavigationBarView(mDisplayId);
+            if (navigationBarView != null) {
+                navigationBarView.onStatusBarPanelStateChanged();
+            }
+            mShadeExpansionStateManager.onQsExpansionChanged(expanded);
         }
     }
 
@@ -3718,6 +3737,11 @@
         setListening(true);
     }
 
+    @VisibleForTesting
+    void setTouchSlopExceeded(boolean isTouchSlopExceeded) {
+        mTouchSlopExceeded = isTouchSlopExceeded;
+    }
+
     public void setOverExpansion(float overExpansion) {
         if (overExpansion == mOverExpansion) {
             return;
@@ -3906,12 +3930,16 @@
         switch (mBarState) {
             case KEYGUARD:
                 if (!mDozingOnDown) {
-                    if (mUpdateMonitor.isFaceEnrolled()
-                            && !mUpdateMonitor.isFaceDetectionRunning()
-                            && !mUpdateMonitor.getUserCanSkipBouncer(
-                            KeyguardUpdateMonitor.getCurrentUser())) {
-                        mUpdateMonitor.requestFaceAuth(true,
-                                FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+                    mShadeLog.v("onMiddleClicked on Keyguard, mDozingOnDown: false");
+                    // Try triggering face auth, this "might" run. Check
+                    // KeyguardUpdateMonitor#shouldListenForFace to see when face auth won't run.
+                    boolean didFaceAuthRun = mUpdateMonitor.requestFaceAuth(true,
+                            FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+
+                    if (didFaceAuthRun) {
+                        mUpdateMonitor.requestActiveUnlock(
+                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                                "lockScreenEmptySpaceTap");
                     } else {
                         mLockscreenGestureLogger.write(MetricsEvent.ACTION_LS_HINT,
                                 0 /* lengthDp - N/A */, 0 /* velocityDp - N/A */);
@@ -3919,11 +3947,6 @@
                                 .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
                         startUnlockHintAnimation();
                     }
-                    if (mUpdateMonitor.isFaceEnrolled()) {
-                        mUpdateMonitor.requestActiveUnlock(
-                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
-                                "lockScreenEmptySpaceTap");
-                    }
                 }
                 return true;
             case StatusBarState.SHADE_LOCKED:
@@ -4776,6 +4799,7 @@
         mAmbientState.setSwipingUp(false);
         if ((mTracking && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop
                 || Math.abs(y - mInitialExpandY) > mTouchSlop
+                || (!isFullyExpanded() && !isFullyCollapsed())
                 || event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
             mVelocityTracker.computeCurrentVelocity(1000);
             float vel = mVelocityTracker.getYVelocity();
@@ -5173,7 +5197,8 @@
      */
     public void updatePanelExpansionAndVisibility() {
         mShadeExpansionStateManager.onPanelExpansionChanged(
-                mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                mExpandedFraction, isExpanded(),
+                mTracking, mExpansionDragDownAmountPx);
         updateVisibility();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 1d92105..66a22f4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -135,7 +135,8 @@
             DumpManager dumpManager,
             KeyguardStateController keyguardStateController,
             ScreenOffAnimationController screenOffAnimationController,
-            AuthController authController) {
+            AuthController authController,
+            ShadeExpansionStateManager shadeExpansionStateManager) {
         mContext = context;
         mWindowManager = windowManager;
         mActivityManager = activityManager;
@@ -156,6 +157,7 @@
                 .addCallback(mStateListener,
                         SysuiStatusBarStateController.RANK_STATUS_BAR_WINDOW_CONTROLLER);
         configurationController.addCallback(this);
+        shadeExpansionStateManager.addQsExpansionListener(this::onQsExpansionChanged);
 
         float desiredPreferredRefreshRate = context.getResources()
                 .getInteger(R.integer.config_keyguardRefreshRate);
@@ -607,8 +609,7 @@
         apply(mCurrentState);
     }
 
-    @Override
-    public void setQsExpanded(boolean expanded) {
+    private void onQsExpansionChanged(Boolean expanded) {
         mCurrentState.mQsExpanded = expanded;
         apply(mCurrentState);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index d6f0de8..73c6d50 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -36,17 +36,12 @@
     private val navigationModeController: NavigationModeController,
     private val overviewProxyService: OverviewProxyService,
     private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
+    private val shadeExpansionStateManager: ShadeExpansionStateManager,
     private val featureFlags: FeatureFlags,
     @Main private val delayableExecutor: DelayableExecutor
 ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
 
-    var qsExpanded = false
-        set(value) {
-            if (field != value) {
-                field = value
-                mView.invalidate()
-            }
-        }
+    private var qsExpanded = false
     private var splitShadeEnabled = false
     private var isQSDetailShowing = false
     private var isQSCustomizing = false
@@ -71,6 +66,13 @@
             taskbarVisible = visible
         }
     }
+    private val shadeQsExpansionListener: ShadeQsExpansionListener =
+        ShadeQsExpansionListener { isQsExpanded ->
+            if (qsExpanded != isQsExpanded) {
+                qsExpanded = isQsExpanded
+                mView.invalidate()
+            }
+        }
 
     // With certain configuration changes (like light/dark changes), the nav bar will disappear
     // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value
@@ -106,6 +108,7 @@
     public override fun onViewAttached() {
         updateResources()
         overviewProxyService.addCallback(taskbarVisibilityListener)
+        shadeExpansionStateManager.addQsExpansionListener(shadeQsExpansionListener)
         mView.setInsetsChangedListener(delayedInsetSetter)
         mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
         mView.setConfigurationChangedListener { updateResources() }
@@ -113,6 +116,7 @@
 
     override fun onViewDetached() {
         overviewProxyService.removeCallback(taskbarVisibilityListener)
+        shadeExpansionStateManager.removeQsExpansionListener(shadeQsExpansionListener)
         mView.removeOnInsetsChangedListener()
         mView.removeQSFragmentAttachedListener()
         mView.setConfigurationChangedListener(null)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index f617d47..7bba74a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.FloatRange
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.Compile
+import java.util.concurrent.CopyOnWriteArrayList
 import javax.inject.Inject
 
 /**
@@ -31,12 +32,14 @@
 @SysUISingleton
 class ShadeExpansionStateManager @Inject constructor() {
 
-    private val expansionListeners = mutableListOf<ShadeExpansionListener>()
-    private val stateListeners = mutableListOf<ShadeStateListener>()
+    private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>()
+    private val qsExpansionListeners = CopyOnWriteArrayList<ShadeQsExpansionListener>()
+    private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>()
 
     @PanelState private var state: Int = STATE_CLOSED
     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
     private var expanded: Boolean = false
+    private var qsExpanded: Boolean = false
     private var tracking: Boolean = false
     private var dragDownPxAmount: Float = 0f
 
@@ -57,6 +60,15 @@
         expansionListeners.remove(listener)
     }
 
+    fun addQsExpansionListener(listener: ShadeQsExpansionListener) {
+        qsExpansionListeners.add(listener)
+        listener.onQsExpansionChanged(qsExpanded)
+    }
+
+    fun removeQsExpansionListener(listener: ShadeQsExpansionListener) {
+        qsExpansionListeners.remove(listener)
+    }
+
     /** Adds a listener that will be notified when the panel state has changed. */
     fun addStateListener(listener: ShadeStateListener) {
         stateListeners.add(listener)
@@ -126,6 +138,14 @@
         expansionListeners.forEach { it.onPanelExpansionChanged(expansionChangeEvent) }
     }
 
+    /** Called when the quick settings expansion changes to fully expanded or collapsed. */
+    fun onQsExpansionChanged(qsExpanded: Boolean) {
+        this.qsExpanded = qsExpanded
+
+        debugLog("qsExpanded=$qsExpanded")
+        qsExpansionListeners.forEach { it.onQsExpansionChanged(qsExpanded) }
+    }
+
     /** Updates the panel state if necessary. */
     fun updateState(@PanelState state: Int) {
         debugLog(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
new file mode 100644
index 0000000..14882b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeQsExpansionListener.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+/** A listener interface to be notified of expansion events for the quick settings panel. */
+fun interface ShadeQsExpansionListener {
+    /**
+     * Invoked whenever the quick settings expansion changes, when it is fully collapsed or expanded
+     */
+    fun onQsExpansionChanged(isQsExpanded: Boolean)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
new file mode 100644
index 0000000..09019a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.shade.data.repository
+
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.model.ShadeModel
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/** Business logic for shade interactions */
+@SysUISingleton
+class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) {
+
+    val shadeModel: Flow<ShadeModel> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : ShadeExpansionListener {
+                        override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
+                            // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field.
+                            // It is too noisy and produces extra events that consumers won't care
+                            // about
+                            val info =
+                                ShadeModel(
+                                    expansionAmount = event.fraction,
+                                    isExpanded = event.expanded,
+                                    isUserDragging = event.tracking
+                                )
+                            trySendWithFailureLogging(info, TAG, "updated shade expansion info")
+                        }
+                    }
+
+                shadeExpansionStateManager.addExpansionListener(callback)
+                trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info")
+
+                awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) }
+            }
+            .distinctUntilChanged()
+
+    companion object {
+        private const val TAG = "ShadeRepository"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
new file mode 100644
index 0000000..ce0f4283
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.shade.domain.model
+
+import android.annotation.FloatRange
+
+/** Information about shade (NotificationPanel) expansion */
+data class ShadeModel(
+    /** 0 when collapsed, 1 when fully expanded. */
+    @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f,
+    /** Whether the panel should be considered expanded */
+    val isExpanded: Boolean = false,
+    /** Whether the user is actively dragging the panel. */
+    val isUserDragging: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 9d4a27c..4ae0f6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -67,12 +67,15 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.GcUtils;
 import com.android.internal.view.AppearanceRegion;
+import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.policy.CallbackController;
 import com.android.systemui.tracing.ProtoTracer;
 
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
@@ -182,6 +185,7 @@
     private int mLastUpdatedImeDisplayId = INVALID_DISPLAY;
     private ProtoTracer mProtoTracer;
     private final @Nullable CommandRegistry mRegistry;
+    private final @Nullable DumpHandler mDumpHandler;
 
     /**
      * These methods are called back on the main thread.
@@ -471,12 +475,18 @@
     }
 
     public CommandQueue(Context context) {
-        this(context, null, null);
+        this(context, null, null, null);
     }
 
-    public CommandQueue(Context context, ProtoTracer protoTracer, CommandRegistry registry) {
+    public CommandQueue(
+            Context context,
+            ProtoTracer protoTracer,
+            CommandRegistry registry,
+            DumpHandler dumpHandler
+    ) {
         mProtoTracer = protoTracer;
         mRegistry = registry;
+        mDumpHandler = dumpHandler;
         context.getSystemService(DisplayManager.class).registerDisplayListener(this, mHandler);
         // We always have default display.
         setDisabled(DEFAULT_DISPLAY, DISABLE_NONE, DISABLE2_NONE);
@@ -1175,6 +1185,35 @@
     }
 
     @Override
+    public void dumpProto(String[] args, ParcelFileDescriptor pfd) {
+        final FileDescriptor fd = pfd.getFileDescriptor();
+        // This is mimicking Binder#dumpAsync, but on this side of the binder. Might be possible
+        // to just throw this work onto the handler just like the other messages
+        Thread thr = new Thread("Sysui.dumpProto") {
+            public void run() {
+                try {
+                    if (mDumpHandler == null) {
+                        return;
+                    }
+                    // We won't be using the PrintWriter.
+                    OutputStream o = new OutputStream() {
+                        @Override
+                        public void write(int b) {}
+                    };
+                    mDumpHandler.dump(fd, new PrintWriter(o), args);
+                } finally {
+                    try {
+                        // Close the file descriptor so the TransferPipe finishes its thread
+                        pfd.close();
+                    } catch (Exception e) {
+                    }
+                }
+            }
+        };
+        thr.start();
+    }
+
+    @Override
     public void runGcForTest() {
         // Gc sysui
         GcUtils.runGcAndFinalizersSync();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt
index 886ad68..5fb5002 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt
@@ -5,7 +5,7 @@
 import android.util.MathUtils
 import com.android.systemui.R
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.shade.NotificationPanelViewController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import dagger.assisted.Assisted
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 8006931..a2e4536 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QS
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 4be5a1a..ced725e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -48,9 +48,9 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaData;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.SmartspaceMediaData;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 0c9e1ec..e21acb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -92,9 +92,6 @@
     /** Sets the state of whether the keyguard is fading away or not. */
     default void setKeyguardFadingAway(boolean keyguardFadingAway) {}
 
-    /** Sets the state of whether the quick settings is expanded or not. */
-    default void setQsExpanded(boolean expanded) {}
-
     /** Sets the state of whether the user activities are forced or not. */
     default void setForceUserActivity(boolean forceUserActivity) {}
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index f961984..87ef92a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -40,6 +40,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -110,8 +111,8 @@
         setClipChildren(false);
         setClipToPadding(false);
         mShelfIcons.setIsStaticLayout(false);
-        setBottomRoundness(1.0f, false /* animate */);
-        setTopRoundness(1f, false /* animate */);
+        requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
+        requestTopRoundness(1f, false, SourceType.DefaultValue);
 
         // Setting this to first in section to get the clipping to the top roundness correct. This
         // value determines the way we are clipping to the top roundness of the overall shade
@@ -413,7 +414,7 @@
                     if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
                         // only if the first icon is fully in the shelf we want to clip to it!
                         backgroundTop = (int) (child.getTranslationY() - getTranslationY());
-                        firstElementRoundness = expandableRow.getCurrentTopRoundness();
+                        firstElementRoundness = expandableRow.getTopRoundness();
                     }
                 }
 
@@ -507,28 +508,36 @@
             // Round bottom corners within animation bounds
             final float changeFraction = MathUtils.saturate(
                     (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
-            anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction,
-                    false /* animate */);
+            anv.requestBottomRoundness(
+                    anv.isLastInSection() ? 1f : changeFraction,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
 
         } else if (viewEnd < cornerAnimationTop) {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius,
-                    false /* animate */);
+            anv.requestBottomRoundness(
+                    anv.isLastInSection() ? 1f : smallCornerRadius,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
         }
 
         if (viewStart >= cornerAnimationTop) {
             // Round top corners within animation bounds
             final float changeFraction = MathUtils.saturate(
                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
-            anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction,
-                    false /* animate */);
+            anv.requestTopRoundness(
+                    anv.isFirstInSection() ? 1f : changeFraction,
+                    false,
+                    SourceType.OnScroll);
 
         } else if (viewStart < cornerAnimationTop) {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius,
-                    false /* animate */);
+            anv.requestTopRoundness(
+                    anv.isFirstInSection() ? 1f : smallCornerRadius,
+                    false,
+                    SourceType.OnScroll);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
index 8222c9d..c630feb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
@@ -68,6 +69,7 @@
     configurationController: ConfigurationController,
     private val statusBarStateController: StatusBarStateController,
     private val falsingManager: FalsingManager,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
     private val falsingCollector: FalsingCollector,
     dumpManager: DumpManager
@@ -126,6 +128,13 @@
                 initResources(context)
             }
         })
+
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            if (qsExpanded != isQsExpanded) {
+                qsExpanded = isQsExpanded
+            }
+        }
+
         mPowerManager = context.getSystemService(PowerManager::class.java)
         dumpManager.registerDumpable(this)
     }
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 11e3d17..eacb18e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -29,8 +29,9 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaDataManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.carrier.QSCarrierGroupController;
@@ -181,8 +182,10 @@
     static CommandQueue provideCommandQueue(
             Context context,
             ProtoTracer protoTracer,
-            CommandRegistry registry) {
-        return new CommandQueue(context, protoTracer, registry);
+            CommandRegistry registry,
+            DumpHandler dumpHandler
+    ) {
+        return new CommandQueue(context, protoTracer, registry, dumpHandler);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index d88f07c..143c697 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -25,11 +25,12 @@
 import android.view.View
 import android.widget.FrameLayout
 import com.android.internal.annotations.GuardedBy
-import com.android.systemui.animation.Interpolators
 import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
@@ -42,7 +43,6 @@
 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
 import com.android.systemui.util.leak.RotationUtils.Rotation
-
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -62,12 +62,13 @@
  */
 
 @SysUISingleton
-class PrivacyDotViewController @Inject constructor(
+open class PrivacyDotViewController @Inject constructor(
     @Main private val mainExecutor: Executor,
     private val stateController: StatusBarStateController,
     private val configurationController: ConfigurationController,
     private val contentInsetsProvider: StatusBarContentInsetsProvider,
-    private val animationScheduler: SystemStatusAnimationScheduler
+    private val animationScheduler: SystemStatusAnimationScheduler,
+    shadeExpansionStateManager: ShadeExpansionStateManager
 ) {
     private lateinit var tl: View
     private lateinit var tr: View
@@ -75,7 +76,8 @@
     private lateinit var br: View
 
     // Only can be modified on @UiThread
-    private var currentViewState: ViewState = ViewState()
+    var currentViewState: ViewState = ViewState()
+        get() = field
 
     @GuardedBy("lock")
     private var nextViewState: ViewState = currentViewState.copy()
@@ -128,21 +130,25 @@
                 updateStatusBarState()
             }
         })
+
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            dlog("setQsExpanded $isQsExpanded")
+            synchronized(lock) {
+                nextViewState = nextViewState.copy(qsExpanded = isQsExpanded)
+            }
+        }
     }
 
     fun setUiExecutor(e: DelayableExecutor) {
         uiExecutor = e
     }
 
-    fun setShowingListener(l: ShowingListener?) {
-        showingListener = l
+    fun getUiExecutor(): DelayableExecutor? {
+        return uiExecutor
     }
 
-    fun setQsExpanded(expanded: Boolean) {
-        dlog("setQsExpanded $expanded")
-        synchronized(lock) {
-            nextViewState = nextViewState.copy(qsExpanded = expanded)
-        }
+    fun setShowingListener(l: ShowingListener?) {
+        showingListener = l
     }
 
     @UiThread
@@ -175,7 +181,7 @@
     }
 
     @UiThread
-    private fun hideDotView(dot: View, animate: Boolean) {
+    fun hideDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.animate()
@@ -194,7 +200,7 @@
     }
 
     @UiThread
-    private fun showDotView(dot: View, animate: Boolean) {
+    fun showDotView(dot: View, animate: Boolean) {
         dot.clearAnimation()
         if (animate) {
             dot.visibility = View.VISIBLE
@@ -507,6 +513,13 @@
             state.designatedCorner?.contentDescription = state.contentDescription
         }
 
+        updateDotView(state)
+
+        currentViewState = state
+    }
+
+    @UiThread
+    open fun updateDotView(state: ViewState) {
         val shouldShow = state.shouldShowDot()
         if (shouldShow != currentViewState.shouldShowDot()) {
             if (shouldShow && state.designatedCorner != null) {
@@ -515,8 +528,6 @@
                 hideDotView(state.designatedCorner, true)
             }
         }
-
-        currentViewState = state
     }
 
     private val systemStatusAnimationCallback: SystemStatusAnimationCallback =
@@ -620,7 +631,7 @@
     }
 }
 
-private data class ViewState(
+data class ViewState(
     val viewInitialized: Boolean = false,
 
     val systemPrivacyEventIsActive: Boolean = false,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index dfba8cd..fc984618 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -119,6 +119,7 @@
                     regionSamplingEnabled,
                     updateFun
             )
+            initializeTextColors(regionSamplingInstance)
             regionSamplingInstance.startRegionSampler()
             regionSamplingInstances.put(v, regionSamplingInstance)
             connectSession()
@@ -362,18 +363,20 @@
         }
     }
 
+    private fun initializeTextColors(regionSamplingInstance: RegionSamplingInstance) {
+        val lightThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_LightWallpaper)
+        val darkColor = Utils.getColorAttrDefaultColor(lightThemeContext, R.attr.wallpaperTextColor)
+
+        val darkThemeContext = ContextThemeWrapper(context, R.style.Theme_SystemUI)
+        val lightColor = Utils.getColorAttrDefaultColor(darkThemeContext, R.attr.wallpaperTextColor)
+
+        regionSamplingInstance.setForegroundColors(lightColor, darkColor)
+    }
+
     private fun updateTextColorFromRegionSampler() {
         smartspaceViews.forEach {
-            val isRegionDark = regionSamplingInstances.getValue(it).currentRegionDarkness()
-            val themeID = if (isRegionDark.isDark) {
-                R.style.Theme_SystemUI
-            } else {
-                R.style.Theme_SystemUI_LightWallpaper
-            }
-            val themedContext = ContextThemeWrapper(context, themeID)
-            val wallpaperTextColor =
-                    Utils.getColorAttrDefaultColor(themedContext, R.attr.wallpaperTextColor)
-            it.setPrimaryTextColor(wallpaperTextColor)
+            val textColor = regionSamplingInstances.getValue(it).currentForegroundColor()
+            it.setPrimaryTextColor(textColor)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 7fbdd35..2734511 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -17,40 +17,27 @@
 package com.android.systemui.statusbar.notification
 
 import android.content.Context
-import android.util.Log
-import android.widget.Toast
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class NotifPipelineFlags @Inject constructor(
     val context: Context,
     val featureFlags: FeatureFlags
 ) {
-    fun checkLegacyPipelineEnabled(): Boolean {
-        if (Compile.IS_DEBUG) {
-            Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show()
-        }
-        if (featureFlags.isEnabled(Flags.NEW_PIPELINE_CRASH_ON_CALL_TO_OLD_PIPELINE)) {
-            throw RuntimeException("Old pipeline code running with new pipeline enabled")
-        } else {
-            Log.d("NotifPipeline", "Old pipeline code running with new pipeline enabled",
-                    Exception())
-        }
-        return false
-    }
-
     fun isDevLoggingEnabled(): Boolean =
         featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
 
-    fun isSmartspaceDedupingEnabled(): Boolean =
-            featureFlags.isEnabled(Flags.SMARTSPACE) &&
-                    featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
-
-    fun removeUnrankedNotifs(): Boolean =
-        featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS)
+    fun isSmartspaceDedupingEnabled(): Boolean = featureFlags.isEnabled(Flags.SMARTSPACE)
 
     fun fullScreenIntentRequiresKeyguard(): Boolean =
         featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
+
+    val isStabilityIndexFixEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
+    }
+
+    val isSemiStableSortEnabled: Boolean by lazy {
+        featureFlags.isEnabled(Flags.SEMI_STABLE_SORT)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 553826d..0d35fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -70,8 +70,8 @@
         val height = max(0, notification.actualHeight - notification.clipBottomAmount)
         val location = notification.locationOnScreen
 
-        val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
-        val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
+        val clipStartLocation = notificationListContainer.topClippingStartLocation
+        val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0)
         val windowTop = location[1] + roundedTopClipping
         val topCornerRadius = if (roundedTopClipping > 0) {
             // Because the rounded Rect clipping is complex, we start the top rounding at
@@ -80,7 +80,7 @@
             // if we'd like to have this perfect, but this is close enough.
             0f
         } else {
-            notification.currentBackgroundRadiusTop
+            notification.topCornerRadius
         }
         val params = LaunchAnimationParameters(
             top = windowTop,
@@ -88,7 +88,7 @@
             left = location[0],
             right = location[0] + notification.width,
             topCornerRadius = topCornerRadius,
-            bottomCornerRadius = notification.currentBackgroundRadiusBottom
+            bottomCornerRadius = notification.bottomCornerRadius
         )
 
         params.startTranslationZ = notification.translationZ
@@ -97,8 +97,8 @@
         params.startClipTopAmount = notification.clipTopAmount
         if (notification.isChildInGroup) {
             params.startNotificationTop += notification.notificationParent.translationY
-            val parentRoundedClip = Math.max(
-                clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
+            val locationOnScreen = notification.notificationParent.locationOnScreen[1]
+            val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0)
             params.parentStartRoundedTopClipping = parentRoundedClip
 
             val parentClip = notification.notificationParent.clipTopAmount
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 7242506..d97b712 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -18,8 +18,10 @@
 
 import android.animation.ObjectAnimator
 import android.util.FloatProperty
+import com.android.systemui.Dumpable
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionChangeEvent
 import com.android.systemui.shade.ShadeExpansionListener
@@ -32,17 +34,20 @@
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
+import java.io.PrintWriter
 import javax.inject.Inject
 import kotlin.math.min
 
 @SysUISingleton
 class NotificationWakeUpCoordinator @Inject constructor(
+    dumpManager: DumpManager,
     private val mHeadsUpManager: HeadsUpManager,
     private val statusBarStateController: StatusBarStateController,
     private val bypassController: KeyguardBypassController,
     private val dozeParameters: DozeParameters,
     private val screenOffAnimationController: ScreenOffAnimationController
-) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener {
+) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, ShadeExpansionListener,
+    Dumpable {
 
     private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>(
         "notificationVisibility") {
@@ -60,6 +65,7 @@
 
     private var mLinearDozeAmount: Float = 0.0f
     private var mDozeAmount: Float = 0.0f
+    private var mDozeAmountSource: String = "init"
     private var mNotificationVisibleAmount = 0.0f
     private var mNotificationsVisible = false
     private var mNotificationsVisibleForExpansion = false
@@ -142,6 +148,7 @@
         }
 
     init {
+        dumpManager.registerDumpable(this)
         mHeadsUpManager.addListener(this)
         statusBarStateController.addCallback(this)
         addListener(object : WakeUpListener {
@@ -248,13 +255,14 @@
             // Let's notify the scroller that an animation started
             notifyAnimationStart(mLinearDozeAmount == 1.0f)
         }
-        setDozeAmount(linear, eased)
+        setDozeAmount(linear, eased, source = "StatusBar")
     }
 
-    fun setDozeAmount(linear: Float, eased: Float) {
+    fun setDozeAmount(linear: Float, eased: Float, source: String) {
         val changed = linear != mLinearDozeAmount
         mLinearDozeAmount = linear
         mDozeAmount = eased
+        mDozeAmountSource = source
         mStackScrollerController.setDozeAmount(mDozeAmount)
         updateHideAmount()
         if (changed && linear == 0.0f) {
@@ -271,7 +279,7 @@
             // undefined state, so it's an indication that we should do state cleanup. We override
             // the doze amount to 0f (not dozing) so that the notifications are no longer hidden.
             // See: UnlockedScreenOffAnimationController.onFinishedWakingUp()
-            setDozeAmount(0f, 0f)
+            setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)")
         }
 
         if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) {
@@ -311,12 +319,11 @@
      */
     private fun overrideDozeAmountIfBypass(): Boolean {
         if (bypassController.bypassEnabled) {
-            var amount = 1.0f
-            if (statusBarStateController.state == StatusBarState.SHADE ||
-                statusBarStateController.state == StatusBarState.SHADE_LOCKED) {
-                amount = 0.0f
+            if (statusBarStateController.state == StatusBarState.KEYGUARD) {
+                setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
+            } else {
+                setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
             }
-            setDozeAmount(amount, amount)
             return true
         }
         return false
@@ -332,7 +339,7 @@
      */
     private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean {
         if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) {
-            setDozeAmount(1f, 1f)
+            setDozeAmount(1f, 1f, source = "Override: animating screen off")
             return true
         }
 
@@ -414,6 +421,26 @@
     private fun shouldAnimateVisibility() =
             dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
 
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("mLinearDozeAmount: $mLinearDozeAmount")
+        pw.println("mDozeAmount: $mDozeAmount")
+        pw.println("mDozeAmountSource: $mDozeAmountSource")
+        pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
+        pw.println("mNotificationsVisible: $mNotificationsVisible")
+        pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
+        pw.println("mVisibilityAmount: $mVisibilityAmount")
+        pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount")
+        pw.println("pulseExpanding: $pulseExpanding")
+        pw.println("state: ${StatusBarState.toString(state)}")
+        pw.println("fullyAwake: $fullyAwake")
+        pw.println("wakingUp: $wakingUp")
+        pw.println("willWakeUp: $willWakeUp")
+        pw.println("collapsedEnoughToHide: $collapsedEnoughToHide")
+        pw.println("pulsing: $pulsing")
+        pw.println("notificationsFullyHidden: $notificationsFullyHidden")
+        pw.println("canShowPulsingHuns: $canShowPulsingHuns")
+    }
+
     interface WakeUpListener {
         /**
          * Called whenever the notifications are fully hidden or shown
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
new file mode 100644
index 0000000..ed7f648
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -0,0 +1,284 @@
+package com.android.systemui.statusbar.notification
+
+import android.util.FloatProperty
+import android.view.View
+import androidx.annotation.FloatRange
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.stack.AnimationProperties
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import kotlin.math.abs
+
+/**
+ * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
+ *
+ * To request a roundness value, an [SourceType] must be specified. In case more origins require
+ * different roundness, for the same property, the maximum value will always be chosen.
+ *
+ * It also returns the current radius for all corners ([updatedRadii]).
+ */
+interface Roundable {
+    /** Properties required for a Roundable */
+    val roundableState: RoundableState
+
+    /** Current top roundness */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    @JvmDefault
+    val topRoundness: Float
+        get() = roundableState.topRoundness
+
+    /** Current bottom roundness */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    @JvmDefault
+    val bottomRoundness: Float
+        get() = roundableState.bottomRoundness
+
+    /** Max radius in pixel */
+    @JvmDefault
+    val maxRadius: Float
+        get() = roundableState.maxRadius
+
+    /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
+    @JvmDefault
+    val topCornerRadius: Float
+        get() = topRoundness * maxRadius
+
+    /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
+    @JvmDefault
+    val bottomCornerRadius: Float
+        get() = bottomRoundness * maxRadius
+
+    /** Get and update the current radii */
+    @JvmDefault
+    val updatedRadii: FloatArray
+        get() =
+            roundableState.radiiBuffer.also { radii ->
+                updateRadii(
+                    topCornerRadius = topCornerRadius,
+                    bottomCornerRadius = bottomCornerRadius,
+                    radii = radii,
+                )
+            }
+
+    /**
+     * Request the top roundness [value] for a specific [sourceType].
+     *
+     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value a value between 0f and 1f.
+     * @param animate true if it should animate to that value.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestTopRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        animate: Boolean,
+        sourceType: SourceType,
+    ): Boolean {
+        val roundnessMap = roundableState.topRoundnessMap
+        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+        if (value == 0f) {
+            // we should only take the largest value, and since the smallest value is 0f, we can
+            // remove this value from the list. In the worst case, the list is empty and the
+            // default value is 0f.
+            roundnessMap.remove(sourceType)
+        } else {
+            roundnessMap[sourceType] = value
+        }
+        val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+        if (lastValue != newValue) {
+            val wasAnimating = roundableState.isTopAnimating()
+
+            // Fail safe:
+            // when we've been animating previously and we're now getting an update in the
+            // other direction, make sure to animate it too, otherwise, the localized updating
+            // may make the start larger than 1.0.
+            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+            roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Request the bottom roundness [value] for a specific [sourceType].
+     *
+     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value value between 0f and 1f.
+     * @param animate true if it should animate to that value.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestBottomRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        animate: Boolean,
+        sourceType: SourceType,
+    ): Boolean {
+        val roundnessMap = roundableState.bottomRoundnessMap
+        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+        if (value == 0f) {
+            // we should only take the largest value, and since the smallest value is 0f, we can
+            // remove this value from the list. In the worst case, the list is empty and the
+            // default value is 0f.
+            roundnessMap.remove(sourceType)
+        } else {
+            roundnessMap[sourceType] = value
+        }
+        val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+        if (lastValue != newValue) {
+            val wasAnimating = roundableState.isBottomAnimating()
+
+            // Fail safe:
+            // when we've been animating previously and we're now getting an update in the
+            // other direction, make sure to animate it too, otherwise, the localized updating
+            // may make the start larger than 1.0.
+            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+            roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
+            return true
+        }
+        return false
+    }
+
+    /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
+    @JvmDefault
+    fun applyRoundness() {
+        roundableState.targetView.invalidate()
+    }
+
+    /** @return true if top or bottom roundness is not zero. */
+    @JvmDefault
+    fun hasRoundedCorner(): Boolean {
+        return topRoundness != 0f || bottomRoundness != 0f
+    }
+
+    /**
+     * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
+     * [android.graphics.Path.addRoundRect].
+     *
+     * This method reuses the previous [radii] for performance reasons.
+     */
+    @JvmDefault
+    fun updateRadii(
+        topCornerRadius: Float,
+        bottomCornerRadius: Float,
+        radii: FloatArray,
+    ) {
+        if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")
+
+        if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
+            (0..3).forEach { radii[it] = topCornerRadius }
+            (4..7).forEach { radii[it] = bottomCornerRadius }
+        }
+    }
+}
+
+/**
+ * State object for a `Roundable` class.
+ * @param targetView Will handle the [AnimatableProperty]
+ * @param roundable Target of the radius animation
+ * @param maxRadius Max corner radius in pixels
+ */
+class RoundableState(
+    internal val targetView: View,
+    roundable: Roundable,
+    internal val maxRadius: Float,
+) {
+    /** Animatable for top roundness */
+    private val topAnimatable = topAnimatable(roundable)
+
+    /** Animatable for bottom roundness */
+    private val bottomAnimatable = bottomAnimatable(roundable)
+
+    /** Current top roundness. Use [setTopRoundness] to update this value */
+    @set:FloatRange(from = 0.0, to = 1.0)
+    internal var topRoundness = 0f
+        private set
+
+    /** Current bottom roundness. Use [setBottomRoundness] to update this value */
+    @set:FloatRange(from = 0.0, to = 1.0)
+    internal var bottomRoundness = 0f
+        private set
+
+    /** Last requested top roundness associated by [SourceType] */
+    internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
+
+    /** Last requested bottom roundness associated by [SourceType] */
+    internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()
+
+    /** Last cached radii */
+    internal val radiiBuffer = FloatArray(8)
+
+    /** Is top roundness animation in progress? */
+    internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)
+
+    /** Is bottom roundness animation in progress? */
+    internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)
+
+    /** Set the current top roundness */
+    internal fun setTopRoundness(
+        value: Float,
+        animated: Boolean = targetView.isShown,
+    ) {
+        PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
+    }
+
+    /** Set the current bottom roundness */
+    internal fun setBottomRoundness(
+        value: Float,
+        animated: Boolean = targetView.isShown,
+    ) {
+        PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
+    }
+
+    companion object {
+        private val DURATION: AnimationProperties =
+            AnimationProperties()
+                .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())
+
+        private fun topAnimatable(roundable: Roundable): AnimatableProperty =
+            AnimatableProperty.from(
+                object : FloatProperty<View>("topRoundness") {
+                    override fun get(view: View): Float = roundable.topRoundness
+
+                    override fun setValue(view: View, value: Float) {
+                        roundable.roundableState.topRoundness = value
+                        roundable.applyRoundness()
+                    }
+                },
+                R.id.top_roundess_animator_tag,
+                R.id.top_roundess_animator_end_tag,
+                R.id.top_roundess_animator_start_tag,
+            )
+
+        private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
+            AnimatableProperty.from(
+                object : FloatProperty<View>("bottomRoundness") {
+                    override fun get(view: View): Float = roundable.bottomRoundness
+
+                    override fun setValue(view: View, value: Float) {
+                        roundable.roundableState.bottomRoundness = value
+                        roundable.applyRoundness()
+                    }
+                },
+                R.id.bottom_roundess_animator_tag,
+                R.id.bottom_roundess_animator_end_tag,
+                R.id.bottom_roundess_animator_start_tag,
+            )
+    }
+}
+
+enum class SourceType {
+    DefaultValue,
+    OnDismissAnimation,
+    OnScroll,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index f8449ae..84ab0d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -68,6 +68,9 @@
      */
     var stableIndex: Int = -1
 
+    /** Access the index of the [section] or -1 if the entry does not have one */
+    val sectionIndex: Int get() = section?.index ?: -1
+
     /** Copies the state of another instance. */
     fun clone(other: ListAttachState) {
         parent = other.parent
@@ -95,11 +98,13 @@
      * This can happen if the entry is removed from a group that was broken up or if the entry was
      * filtered out during any of the filtering steps.
      */
-    fun detach() {
+    fun detach(includingStableIndex: Boolean) {
         parent = null
         section = null
         promoter = null
-        // stableIndex = -1  // TODO(b/241229236): Clear this once we fix the stability fragility
+        if (includingStableIndex) {
+            stableIndex = -1
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 2887f97..df35c9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -602,7 +602,7 @@
 
         mInconsistencyTracker.logNewMissingNotifications(rankingMap);
         mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap);
-        if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) {
+        if (currentEntriesWithoutRankings != null) {
             for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
                 entry.mCancellationReason = REASON_UNKNOWN;
                 tryRemoveNotification(entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index e129ee4..3ae2545 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -54,6 +54,9 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort;
+import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
@@ -96,11 +99,14 @@
     // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
     // TODO replace temp with collection pool for readability
     private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
+    private NotifPipelineFlags mFlags;
     private final boolean mAlwaysLogList;
 
     private List<ListEntry> mNotifList = new ArrayList<>();
     private List<ListEntry> mNewNotifList = new ArrayList<>();
 
+    private final SemiStableSort mSemiStableSort = new SemiStableSort();
+    private final StableOrder<ListEntry> mStableOrder = this::getStableOrderRank;
     private final PipelineState mPipelineState = new PipelineState();
     private final Map<String, GroupEntry> mGroups = new ArrayMap<>();
     private Collection<NotificationEntry> mAllEntries = Collections.emptyList();
@@ -141,6 +147,7 @@
     ) {
         mSystemClock = systemClock;
         mLogger = logger;
+        mFlags = flags;
         mAlwaysLogList = flags.isDevLoggingEnabled();
         mInteractionTracker = interactionTracker;
         mChoreographer = pipelineChoreographer;
@@ -527,7 +534,7 @@
             List<NotifFilter> filters) {
         Trace.beginSection("ShadeListBuilder.filterNotifs");
         final long now = mSystemClock.uptimeMillis();
-        for (ListEntry entry : entries)  {
+        for (ListEntry entry : entries) {
             if (entry instanceof GroupEntry) {
                 final GroupEntry groupEntry = (GroupEntry) entry;
 
@@ -958,7 +965,8 @@
      * filtered out during any of the filtering steps.
      */
     private void annulAddition(ListEntry entry) {
-        entry.getAttachState().detach();
+        // NOTE(b/241229236): Don't clear stableIndex until we fix stability fragility
+        entry.getAttachState().detach(/* includingStableIndex= */ mFlags.isSemiStableSortEnabled());
     }
 
     private void assignSections() {
@@ -978,7 +986,16 @@
 
     private void sortListAndGroups() {
         Trace.beginSection("ShadeListBuilder.sortListAndGroups");
-        // Assign sections to top-level elements and sort their children
+        if (mFlags.isSemiStableSortEnabled()) {
+            sortWithSemiStableSort();
+        } else {
+            sortWithLegacyStability();
+        }
+        Trace.endSection();
+    }
+
+    private void sortWithLegacyStability() {
+        // Sort all groups and the top level list
         for (ListEntry entry : mNotifList) {
             if (entry instanceof GroupEntry) {
                 GroupEntry parent = (GroupEntry) entry;
@@ -991,16 +1008,15 @@
         // Check for suppressed order changes
         if (!getStabilityManager().isEveryChangeAllowed()) {
             mForceReorderable = true;
-            boolean isSorted = isShadeSorted();
+            boolean isSorted = isShadeSortedLegacy();
             mForceReorderable = false;
             if (!isSorted) {
                 getStabilityManager().onEntryReorderSuppressed();
             }
         }
-        Trace.endSection();
     }
 
-    private boolean isShadeSorted() {
+    private boolean isShadeSortedLegacy() {
         if (!isSorted(mNotifList, mTopLevelComparator)) {
             return false;
         }
@@ -1014,6 +1030,43 @@
         return true;
     }
 
+    private void sortWithSemiStableSort() {
+        // Sort each group's children
+        boolean allSorted = true;
+        for (ListEntry entry : mNotifList) {
+            if (entry instanceof GroupEntry) {
+                GroupEntry parent = (GroupEntry) entry;
+                allSorted &= sortGroupChildren(parent.getRawChildren());
+            }
+        }
+        // Sort each section within the top level list
+        mNotifList.sort(mTopLevelComparator);
+        if (!getStabilityManager().isEveryChangeAllowed()) {
+            for (List<ListEntry> subList : getSectionSubLists(mNotifList)) {
+                allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList);
+            }
+            applyNewNotifList();
+        }
+        assignIndexes(mNotifList);
+        if (!allSorted) {
+            // Report suppressed order changes
+            getStabilityManager().onEntryReorderSuppressed();
+        }
+    }
+
+    private Iterable<List<ListEntry>> getSectionSubLists(List<ListEntry> entries) {
+        return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries);
+    }
+
+    private boolean sortGroupChildren(List<NotificationEntry> entries) {
+        if (getStabilityManager().isEveryChangeAllowed()) {
+            entries.sort(mGroupChildrenComparator);
+            return true;
+        } else {
+            return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator);
+        }
+    }
+
     /** Determine whether the items in the list are sorted according to the comparator */
     @VisibleForTesting
     public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
@@ -1036,27 +1089,41 @@
     /**
      * Assign the index of each notification relative to the total order
      */
-    private static void assignIndexes(List<ListEntry> notifList) {
+    private void assignIndexes(List<ListEntry> notifList) {
         if (notifList.size() == 0) return;
         NotifSection currentSection = requireNonNull(notifList.get(0).getSection());
         int sectionMemberIndex = 0;
         for (int i = 0; i < notifList.size(); i++) {
-            ListEntry entry = notifList.get(i);
+            final ListEntry entry = notifList.get(i);
             NotifSection section = requireNonNull(entry.getSection());
             if (section.getIndex() != currentSection.getIndex()) {
                 sectionMemberIndex = 0;
                 currentSection = section;
             }
-            entry.getAttachState().setStableIndex(sectionMemberIndex);
-            if (entry instanceof GroupEntry) {
-                GroupEntry parent = (GroupEntry) entry;
-                for (int j = 0; j < parent.getChildren().size(); j++) {
-                    entry = parent.getChildren().get(j);
-                    entry.getAttachState().setStableIndex(sectionMemberIndex);
-                    sectionMemberIndex++;
+            if (mFlags.isStabilityIndexFixEnabled()) {
+                entry.getAttachState().setStableIndex(sectionMemberIndex++);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    final NotificationEntry summary = parent.getSummary();
+                    if (summary != null) {
+                        summary.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
                 }
+            } else {
+                // This old implementation uses the same index number for the group as the first
+                // child, and fails to assign an index to the summary.  Remove once tested.
+                entry.getAttachState().setStableIndex(sectionMemberIndex);
+                if (entry instanceof GroupEntry) {
+                    final GroupEntry parent = (GroupEntry) entry;
+                    for (NotificationEntry child : parent.getChildren()) {
+                        child.getAttachState().setStableIndex(sectionMemberIndex++);
+                    }
+                }
+                sectionMemberIndex++;
             }
-            sectionMemberIndex++;
         }
     }
 
@@ -1196,7 +1263,7 @@
                 o2.getSectionIndex());
         if (cmp != 0) return cmp;
 
-        cmp = Integer.compare(
+        cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1225,7 +1292,7 @@
 
 
     private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
-        int cmp = Integer.compare(
+        int cmp = mFlags.isSemiStableSortEnabled() ? 0 : Integer.compare(
                 getStableOrderIndex(o1),
                 getStableOrderIndex(o2));
         if (cmp != 0) return cmp;
@@ -1256,9 +1323,25 @@
             // let the stability manager constrain or allow reordering
             return -1;
         }
+        // NOTE(b/241229236): Can't use cleared section index until we fix stability fragility
         return entry.getPreviousAttachState().getStableIndex();
     }
 
+    @Nullable
+    private Integer getStableOrderRank(ListEntry entry) {
+        if (getStabilityManager().isEntryReorderingAllowed(entry)) {
+            // let the stability manager constrain or allow reordering
+            return null;
+        }
+        if (entry.getAttachState().getSectionIndex()
+                != entry.getPreviousAttachState().getSectionIndex()) {
+            // stable index is only valid within the same section; otherwise we allow reordering
+            return null;
+        }
+        final int stableIndex = entry.getPreviousAttachState().getStableIndex();
+        return stableIndex == -1 ? null : stableIndex;
+    }
+
     private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
         final NotifFilter filter = findRejectingFilter(entry, now, filters);
         entry.getAttachState().setExcludingFilter(filter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8f3eb4f..8a31ed9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -18,6 +18,8 @@
 import android.app.Notification
 import android.app.Notification.GROUP_ALERT_SUMMARY
 import android.util.ArrayMap
+import android.util.ArraySet
+import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.NotificationRemoteInputManager
 import com.android.systemui.statusbar.notification.collection.GroupEntry
@@ -70,6 +72,7 @@
     @Main private val mExecutor: DelayableExecutor,
 ) : Coordinator {
     private val mEntriesBindingUntil = ArrayMap<String, Long>()
+    private val mEntriesUpdateTimes = ArrayMap<String, Long>()
     private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
     private lateinit var mNotifPipeline: NotifPipeline
     private var mNow: Long = -1
@@ -264,6 +267,9 @@
         }
         // After this method runs, all posted entries should have been handled (or skipped).
         mPostedEntries.clear()
+
+        // Also take this opportunity to clean up any stale entry update times
+        cleanUpEntryUpdateTimes()
     }
 
     /**
@@ -378,6 +384,9 @@
                 isAlerting = false,
                 isBinding = false,
             )
+
+            // Record the last updated time for this key
+            setUpdateTime(entry, mSystemClock.currentTimeMillis())
         }
 
         /**
@@ -419,6 +428,9 @@
                     cancelHeadsUpBind(posted.entry)
                 }
             }
+
+            // Update last updated time for this entry
+            setUpdateTime(entry, mSystemClock.currentTimeMillis())
         }
 
         /**
@@ -426,6 +438,7 @@
          */
         override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
             mPostedEntries.remove(entry.key)
+            mEntriesUpdateTimes.remove(entry.key)
             cancelHeadsUpBind(entry)
             val entryKey = entry.key
             if (mHeadsUpManager.isAlerting(entryKey)) {
@@ -454,7 +467,12 @@
             // never) in mPostedEntries to need to alert, we need to check every notification
             // known to the pipeline.
             for (entry in mNotifPipeline.allNotifs) {
-                // The only entries we can consider alerting for here are entries that have never
+                // Only consider entries that are recent enough, since we want to apply a fairly
+                // strict threshold for when an entry should be updated via only ranking and not an
+                // app-provided notification update.
+                if (!isNewEnoughForRankingUpdate(entry)) continue
+
+                // The only entries we consider alerting for here are entries that have never
                 // interrupted and that now say they should heads up; if they've alerted in the
                 // past, we don't want to incorrectly alert a second time if there wasn't an
                 // explicit notification update.
@@ -486,6 +504,41 @@
                 (entry.sbn.notification.flags and Notification.FLAG_ONLY_ALERT_ONCE) == 0)
     }
 
+    /**
+     * Sets the updated time for the given entry to the specified time.
+     */
+    @VisibleForTesting
+    fun setUpdateTime(entry: NotificationEntry, time: Long) {
+        mEntriesUpdateTimes[entry.key] = time
+    }
+
+    /**
+     * Checks whether the entry is new enough to be updated via ranking update.
+     * We want to avoid updating an entry too long after it was originally posted/updated when we're
+     * only reacting to a ranking change, as relevant ranking updates are expected to come in
+     * fairly soon after the posting of a notification.
+     */
+    private fun isNewEnoughForRankingUpdate(entry: NotificationEntry): Boolean {
+        // If we don't have an update time for this key, default to "too old"
+        if (!mEntriesUpdateTimes.containsKey(entry.key)) return false
+
+        val updateTime = mEntriesUpdateTimes[entry.key] ?: return false
+        return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
+    }
+
+    private fun cleanUpEntryUpdateTimes() {
+        // Because we won't update entries that are older than this amount of time anyway, clean
+        // up any entries that are too old to notify.
+        val toRemove = ArraySet<String>()
+        for ((key, updateTime) in mEntriesUpdateTimes) {
+            if (updateTime == null ||
+                    (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
+                toRemove.add(key)
+            }
+        }
+        mEntriesUpdateTimes.removeAll(toRemove)
+    }
+
     /** When an action is pressed on a notification, end HeadsUp lifetime extension. */
     private val mActionPressListener = Consumer<NotificationEntry> { entry ->
         if (mNotifsExtendingLifetime.contains(entry)) {
@@ -597,6 +650,9 @@
     companion object {
         private const val TAG = "HeadsUpCoordinator"
         private const val BIND_TIMEOUT = 1000L
+
+        // This value is set to match MAX_SOUND_DELAY_MS in NotificationRecord.
+        private const val MAX_RANKING_UPDATE_DELAY_MS: Long = 2000
     }
 
     data class PostedEntry(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index 2480ff6..0be4bde 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -16,14 +16,14 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
-import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification;
+import static com.android.systemui.media.controls.pipeline.MediaDataManagerKt.isMediaNotification;
 
 import android.os.RemoteException;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 93146f9..d2db622 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -410,7 +410,7 @@
         // Only delay release if the summary is not inflated.
         // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been
         //  done by this point, we can revert back to checking for mInflatingNotifs.contains(...)
-        if (!isInflated(group.getSummary())) {
+        if (group.getSummary() != null && !isInflated(group.getSummary())) {
             mLogger.logDelayingGroupRelease(group, group.getSummary());
             return true;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
new file mode 100644
index 0000000..9ec8e07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSort.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import androidx.annotation.VisibleForTesting
+import kotlin.math.sign
+
+class SemiStableSort {
+    val preallocatedWorkspace by lazy { ArrayList<Any>() }
+    val preallocatedAdditions by lazy { ArrayList<Any>() }
+    val preallocatedMapToIndex by lazy { HashMap<Any, Int>() }
+    val preallocatedMapToIndexComparator: Comparator<Any> by lazy {
+        Comparator.comparingInt { item -> preallocatedMapToIndex[item] ?: -1 }
+    }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be placed into the original [items] list.
+     */
+    fun <T : Any> sort(
+        items: MutableList<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+    ): Boolean =
+        withWorkspace<T, Boolean> { workspace ->
+            val ordered =
+                sortTo(
+                    items,
+                    stableOrder,
+                    comparator,
+                    workspace,
+                )
+            items.clear()
+            items.addAll(workspace)
+            return ordered
+        }
+
+    /**
+     * Sort the given [items] such that items which have a [stableOrder] will all be in that order,
+     * items without a [stableOrder] will be sorted according to the comparator, and the two sets of
+     * items will be combined to have the fewest elements out of order according to the [comparator]
+     * . The result will be put into [output].
+     */
+    fun <T : Any> sortTo(
+        items: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        comparator: Comparator<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        if (DEBUG) println("\n> START from ${items.map { it to stableOrder.getRank(it) }}")
+        // If array already has elements, use subList to ensure we only append
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        items.filterTo(result) { stableOrder.getRank(it) != null }
+        result.sortBy { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(comparator)
+        withAdditions<T> { additions ->
+            items.filterTo(additions) { stableOrder.getRank(it) == null }
+            additions.sortWith(comparator)
+            insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+        }
+        return isOrdered
+    }
+
+    /**
+     * Rearrange the [sortedItems] to enforce that items are in the [stableOrder], and store the
+     * result in [output]. Items with a [stableOrder] will be in that order, items without a
+     * [stableOrder] will remain in same relative order as the input, and the two sets of items will
+     * be combined to have the fewest elements moved from their locations in the original.
+     */
+    fun <T : Any> stabilizeTo(
+        sortedItems: Iterable<T>,
+        stableOrder: StableOrder<in T>,
+        output: MutableList<T>,
+    ): Boolean {
+        // Append to the output array if present
+        val result = output.takeIf { it.isEmpty() } ?: output.subList(output.size, output.size)
+        sortedItems.filterTo(result) { stableOrder.getRank(it) != null }
+        val stableRankComparator = compareBy<T> { stableOrder.getRank(it)!! }
+        val isOrdered = result.isSorted(stableRankComparator)
+        if (!isOrdered) {
+            result.sortWith(stableRankComparator)
+        }
+        if (result.isEmpty()) {
+            sortedItems.filterTo(result) { stableOrder.getRank(it) == null }
+            return isOrdered
+        }
+        withAdditions<T> { additions ->
+            sortedItems.filterTo(additions) { stableOrder.getRank(it) == null }
+            if (additions.isNotEmpty()) {
+                withIndexOfComparator(sortedItems) { comparator ->
+                    insertPreSortedElementsWithFewestMisOrderings(result, additions, comparator)
+                }
+            }
+        }
+        return isOrdered
+    }
+
+    private inline fun <T : Any, R> withWorkspace(block: (ArrayList<T>) -> R): R {
+        preallocatedWorkspace.clear()
+        val result = block(preallocatedWorkspace as ArrayList<T>)
+        preallocatedWorkspace.clear()
+        return result
+    }
+
+    private inline fun <T : Any> withAdditions(block: (ArrayList<T>) -> Unit) {
+        preallocatedAdditions.clear()
+        block(preallocatedAdditions as ArrayList<T>)
+        preallocatedAdditions.clear()
+    }
+
+    private inline fun <T : Any> withIndexOfComparator(
+        sortedItems: Iterable<T>,
+        block: (Comparator<in T>) -> Unit
+    ) {
+        preallocatedMapToIndex.clear()
+        sortedItems.forEachIndexed { i, item -> preallocatedMapToIndex[item] = i }
+        block(preallocatedMapToIndexComparator as Comparator<in T>)
+        preallocatedMapToIndex.clear()
+    }
+
+    companion object {
+
+        /**
+         * This is the core of the algorithm.
+         *
+         * Insert [preSortedAdditions] (the elements to be inserted) into [existing] without
+         * changing the relative order of any elements already in [existing], even though those
+         * elements may be mis-ordered relative to the [comparator], such that the total number of
+         * elements which are ordered incorrectly according to the [comparator] is fewest.
+         */
+        private fun <T> insertPreSortedElementsWithFewestMisOrderings(
+            existing: MutableList<T>,
+            preSortedAdditions: Iterable<T>,
+            comparator: Comparator<in T>,
+        ) {
+            if (DEBUG) println("  To $existing insert $preSortedAdditions with fewest misordering")
+            var iStart = 0
+            preSortedAdditions.forEach { toAdd ->
+                if (DEBUG) println("    need to add $toAdd to $existing, starting at $iStart")
+                var cmpSum = 0
+                var cmpSumMax = 0
+                var iCmpSumMax = iStart
+                if (DEBUG) print("      ")
+                for (i in iCmpSumMax until existing.size) {
+                    val cmp = comparator.compare(toAdd, existing[i]).sign
+                    cmpSum += cmp
+                    if (cmpSum > cmpSumMax) {
+                        cmpSumMax = cmpSum
+                        iCmpSumMax = i + 1
+                    }
+                    if (DEBUG) print("sum[$i]=$cmpSum, ")
+                }
+                if (DEBUG) println("inserting $toAdd at $iCmpSumMax")
+                existing.add(iCmpSumMax, toAdd)
+                iStart = iCmpSumMax + 1
+            }
+        }
+
+        /** Determines if a list is correctly sorted according to the given comparator */
+        @VisibleForTesting
+        fun <T> List<T>.isSorted(comparator: Comparator<T>): Boolean {
+            if (this.size <= 1) {
+                return true
+            }
+            val iterator = this.iterator()
+            var previous = iterator.next()
+            var current: T?
+            while (iterator.hasNext()) {
+                current = iterator.next()
+                if (comparator.compare(previous, current) > 0) {
+                    return false
+                }
+                previous = current
+            }
+            return true
+        }
+    }
+
+    fun interface StableOrder<T> {
+        fun getRank(item: T): Int?
+    }
+}
+
+val DEBUG = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
new file mode 100644
index 0000000..d8f75f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelper.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+object ShadeListBuilderHelper {
+    fun getSectionSubLists(entries: List<ListEntry>): Iterable<List<ListEntry>> =
+        getContiguousSubLists(entries, minLength = 1) { it.sectionIndex }
+
+    inline fun <T : Any, K : Any> getContiguousSubLists(
+        itemList: List<T>,
+        minLength: Int = 1,
+        key: (T) -> K,
+    ): Iterable<List<T>> {
+        val subLists = mutableListOf<List<T>>()
+        val numEntries = itemList.size
+        var currentSectionStartIndex = 0
+        var currentSectionKey: K? = null
+        for (i in 0 until numEntries) {
+            val sectionKey = key(itemList[i])
+            if (currentSectionKey == null) {
+                currentSectionKey = sectionKey
+            } else if (currentSectionKey != sectionKey) {
+                val length = i - currentSectionStartIndex
+                if (length >= minLength) {
+                    subLists.add(itemList.subList(currentSectionStartIndex, i))
+                }
+                currentSectionStartIndex = i
+                currentSectionKey = sectionKey
+            }
+        }
+        val length = numEntries - currentSectionStartIndex
+        if (length >= minLength) {
+            subLists.add(itemList.subList(currentSectionStartIndex, numEntries))
+        }
+        return subLists
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index c5a6921..c4f5a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.notification.interruption;
 
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD;
+import static com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR;
 
 import android.app.NotificationManager;
 import android.content.ContentResolver;
@@ -32,6 +34,8 @@
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -68,10 +72,30 @@
     private final NotificationInterruptLogger mLogger;
     private final NotifPipelineFlags mFlags;
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    private final UiEventLogger mUiEventLogger;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
 
+    public enum NotificationInterruptEvent implements UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "FSI suppressed for suppressive GroupAlertBehavior")
+        FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR(1235),
+
+        @UiEvent(doc = "FSI suppressed for requiring neither HUN nor keyguard")
+        FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD(1236);
+
+        private final int mId;
+
+        NotificationInterruptEvent(int id) {
+            mId = id;
+        }
+
+        @Override
+        public int getId() {
+            return mId;
+        }
+    }
+
     @Inject
     public NotificationInterruptStateProviderImpl(
             ContentResolver contentResolver,
@@ -85,7 +109,8 @@
             NotificationInterruptLogger logger,
             @Main Handler mainHandler,
             NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+            UiEventLogger uiEventLogger) {
         mContentResolver = contentResolver;
         mPowerManager = powerManager;
         mDreamManager = dreamManager;
@@ -97,6 +122,7 @@
         mLogger = logger;
         mFlags = flags;
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
+        mUiEventLogger = uiEventLogger;
         ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -203,7 +229,9 @@
             // b/231322873: Detect and report an event when a notification has both an FSI and a
             // suppressive groupAlertBehavior, and now correctly block the FSI from firing.
             final int uid = entry.getSbn().getUid();
+            final String packageName = entry.getSbn().getPackageName();
             android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "groupAlertBehavior");
+            mUiEventLogger.log(FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR, uid, packageName);
             mLogger.logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
             return false;
         }
@@ -249,7 +277,9 @@
             // Detect the case determined by b/231322873 to launch FSI while device is in use,
             // as blocked by the correct implementation, and report the event.
             final int uid = entry.getSbn().getUid();
+            final String packageName = entry.getSbn().getPackageName();
             android.util.EventLog.writeEvent(0x534e4554, "231322873", uid, "no hun or keyguard");
+            mUiEventLogger.log(FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD, uid, packageName);
             mLogger.logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
index 832a739..0380fff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt
@@ -20,8 +20,9 @@
 /** Describes usage of a notification. */
 data class NotificationMemoryUsage(
     val packageName: String,
-    val notificationId: String,
+    val notificationKey: String,
     val objectUsage: NotificationObjectUsage,
+    val viewUsage: List<NotificationViewUsage>
 )
 
 /**
@@ -39,3 +40,26 @@
     val extender: Int,
     val hasCustomView: Boolean,
 )
+
+enum class ViewType {
+    PUBLIC_VIEW,
+    PRIVATE_CONTRACTED_VIEW,
+    PRIVATE_EXPANDED_VIEW,
+    PRIVATE_HEADS_UP_VIEW,
+    TOTAL
+}
+
+/**
+ * Describes current memory of a notification view hierarchy.
+ *
+ * The values are in bytes.
+ */
+data class NotificationViewUsage(
+    val viewType: ViewType,
+    val smallIcon: Int,
+    val largeIcon: Int,
+    val systemIcons: Int,
+    val style: Int,
+    val customViews: Int,
+    val softwareBitmapsPenalty: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
new file mode 100644
index 0000000..7d39e18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt
@@ -0,0 +1,212 @@
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.app.Person
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.annotation.WorkerThread
+import com.android.systemui.statusbar.notification.NotificationUtils
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Calculates estimated memory usage of [Notification] and [NotificationEntry] objects. */
+internal object NotificationMemoryMeter {
+
+    private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
+    private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
+    private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
+    private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
+    private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+
+    /** Returns a list of memory use entries for currently shown notifications. */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notifications: Collection<NotificationEntry>,
+    ): List<NotificationMemoryUsage> {
+        return notifications
+            .asSequence()
+            .map { entry ->
+                val packageName = entry.sbn.packageName
+                val notificationObjectUsage =
+                    notificationMemoryUse(entry.sbn.notification, hashSetOf())
+                val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row)
+                NotificationMemoryUsage(
+                    packageName,
+                    NotificationUtils.logKey(entry.sbn.key),
+                    notificationObjectUsage,
+                    notificationViewUsage
+                )
+            }
+            .toList()
+    }
+
+    @WorkerThread
+    fun notificationMemoryUse(
+        entry: NotificationEntry,
+        seenBitmaps: HashSet<Int> = hashSetOf(),
+    ): NotificationMemoryUsage {
+        return NotificationMemoryUsage(
+            entry.sbn.packageName,
+            NotificationUtils.logKey(entry.sbn.key),
+            notificationMemoryUse(entry.sbn.notification, seenBitmaps),
+            NotificationMemoryViewWalker.getViewUsage(entry.row)
+        )
+    }
+
+    /**
+     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
+     * inspect Bitmaps in the object and provide summary of memory usage.
+     */
+    @WorkerThread
+    fun notificationMemoryUse(
+        notification: Notification,
+        seenBitmaps: HashSet<Int> = hashSetOf(),
+    ): NotificationObjectUsage {
+        val extras = notification.extras
+        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
+        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
+
+        // Collect memory usage of extra styles
+
+        // Big Picture
+        val bigPictureIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
+        val bigPictureUse =
+            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
+                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
+
+        // People
+        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
+        val peopleUse =
+            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
+
+        // Calling
+        val callingPersonUse =
+            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
+        val verificationIconUse =
+            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
+
+        // Messages
+        val messages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
+            )
+        val messagesUse =
+            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+        val historicMessages =
+            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
+            )
+        val historyicMessagesUse =
+            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
+
+        // Extenders
+        val carExtender = extras.getBundle(CAR_EXTENSIONS)
+        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
+        val carExtenderIcon =
+            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
+
+        val tvExtender = extras.getBundle(TV_EXTENSIONS)
+        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
+
+        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
+        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
+        val wearExtenderBackground =
+            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
+
+        val style = notification.notificationStyle
+        val hasCustomView = notification.contentView != null || notification.bigContentView != null
+        val extrasSize = computeBundleSize(extras)
+
+        return NotificationObjectUsage(
+            smallIcon = smallIconUse,
+            largeIcon = largeIconUse,
+            extras = extrasSize,
+            style = style?.simpleName,
+            styleIcon =
+                bigPictureIconUse +
+                    peopleUse +
+                    callingPersonUse +
+                    verificationIconUse +
+                    messagesUse +
+                    historyicMessagesUse,
+            bigPicture = bigPictureUse,
+            extender =
+                carExtenderSize +
+                    carExtenderIcon +
+                    tvExtenderSize +
+                    wearExtenderSize +
+                    wearExtenderBackground,
+            hasCustomView = hasCustomView
+        )
+    }
+
+    /**
+     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
+     * bitmaps). Can be slow.
+     */
+    private fun computeBundleSize(extras: Bundle): Int {
+        val parcel = Parcel.obtain()
+        try {
+            extras.writeToParcel(parcel, 0)
+            return parcel.dataSize()
+        } finally {
+            parcel.recycle()
+        }
+    }
+
+    /**
+     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
+     * if the key does not exist in extras.
+     */
+    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
+        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
+            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
+            is Icon -> computeIconUse(parcelable, seenBitmaps)
+            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
+            else -> 0
+        }
+    }
+
+    /**
+     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
+     * defined via Uri or a resource.
+     *
+     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
+     */
+    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
+        when (icon?.type) {
+            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
+            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
+            else -> 0
+        }
+
+    /**
+     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
+     * seenBitmaps set, this method returns 0 to avoid double counting.
+     *
+     * @return memory usage of the bitmap in bytes
+     */
+    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
+        val refId = System.identityHashCode(bitmap)
+        if (seenBitmaps?.contains(refId) == true) {
+            return 0
+        }
+
+        seenBitmaps?.add(refId)
+        return bitmap.allocationByteCount
+    }
+
+    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
+        val refId = System.identityHashCode(icon.dataBytes)
+        if (seenBitmaps.contains(refId)) {
+            return 0
+        }
+
+        seenBitmaps.add(refId)
+        return icon.dataLength
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
index 958978e..c09cc43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -17,22 +17,11 @@
 
 package com.android.systemui.statusbar.notification.logging
 
-import android.app.Notification
-import android.app.Person
-import android.graphics.Bitmap
-import android.graphics.drawable.Icon
-import android.os.Bundle
-import android.os.Parcel
-import android.os.Parcelable
 import android.util.Log
-import androidx.annotation.WorkerThread
-import androidx.core.util.contains
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.NotificationUtils
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -46,12 +35,7 @@
 ) : Dumpable {
 
     companion object {
-        private const val TAG = "NotificationMemMonitor"
-        private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
-        private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
-        private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
-        private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
-        private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
+        private const val TAG = "NotificationMemory"
     }
 
     fun init() {
@@ -60,184 +44,123 @@
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
-        currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs)
+                .sortedWith(compareBy({ it.packageName }, { it.notificationKey }))
+        dumpNotificationObjects(pw, memoryUse)
+        dumpNotificationViewUsage(pw, memoryUse)
     }
 
-    @WorkerThread
-    fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
-        return notificationMemoryUse(notificationPipeline.allNotifs)
-    }
-
-    /** Returns a list of memory use entries for currently shown notifications. */
-    @WorkerThread
-    fun notificationMemoryUse(
-        notifications: Collection<NotificationEntry>
-    ): List<NotificationMemoryUsage> {
-        return notifications
-            .asSequence()
-            .map { entry ->
-                val packageName = entry.sbn.packageName
-                val notificationObjectUsage =
-                    computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
-                NotificationMemoryUsage(
-                    packageName,
-                    NotificationUtils.logKey(entry.sbn.key),
-                    notificationObjectUsage
-                )
-            }
-            .toList()
-    }
-
-    /**
-     * Computes the estimated memory usage of a given [Notification] object. It'll attempt to
-     * inspect Bitmaps in the object and provide summary of memory usage.
-     */
-    private fun computeNotificationObjectUse(
-        notification: Notification,
-        seenBitmaps: HashSet<Int>
-    ): NotificationObjectUsage {
-        val extras = notification.extras
-        val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
-        val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
-
-        // Collect memory usage of extra styles
-
-        // Big Picture
-        val bigPictureIconUse =
-            computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
-                computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
-        val bigPictureUse =
-            computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
-                computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
-
-        // People
-        val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
-        val peopleUse =
-            peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
-
-        // Calling
-        val callingPersonUse =
-            computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
-        val verificationIconUse =
-            computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
-
-        // Messages
-        val messages =
-            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
-                extras.getParcelableArray(Notification.EXTRA_MESSAGES)
-            )
-        val messagesUse =
-            messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
-        val historicMessages =
-            Notification.MessagingStyle.Message.getMessagesFromBundleArray(
-                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
-            )
-        val historyicMessagesUse =
-            historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
-
-        // Extenders
-        val carExtender = extras.getBundle(CAR_EXTENSIONS)
-        val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
-        val carExtenderIcon =
-            computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
-
-        val tvExtender = extras.getBundle(TV_EXTENSIONS)
-        val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
-
-        val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
-        val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
-        val wearExtenderBackground =
-            computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
-
-        val style = notification.notificationStyle
-        val hasCustomView = notification.contentView != null || notification.bigContentView != null
-        val extrasSize = computeBundleSize(extras)
-
-        return NotificationObjectUsage(
-            smallIconUse,
-            largeIconUse,
-            extrasSize,
-            style?.simpleName,
-            bigPictureIconUse +
-                peopleUse +
-                callingPersonUse +
-                verificationIconUse +
-                messagesUse +
-                historyicMessagesUse,
-            bigPictureUse,
-            carExtenderSize +
-                carExtenderIcon +
-                tvExtenderSize +
-                wearExtenderSize +
-                wearExtenderBackground,
-            hasCustomView
+    /** Renders a table of notification object usage into passed [PrintWriter]. */
+    private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) {
+        pw.println("Notification Object Usage")
+        pw.println("-----------")
+        pw.println(
+            "Package".padEnd(35) +
+                "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom"
         )
+        pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView")
+        pw.println()
+
+        memoryUse.forEach { use ->
+            pw.println(
+                use.packageName.padEnd(35) +
+                    "\t\t" +
+                    "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" +
+                    (use.objectUsage.style?.take(15) ?: "").padEnd(15) +
+                    "\t\t${use.objectUsage.styleIcon}\t" +
+                    "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" +
+                    "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" +
+                    use.notificationKey
+            )
+        }
+
+        // Calculate totals for easily glanceable summary.
+        data class Totals(
+            var smallIcon: Int = 0,
+            var largeIcon: Int = 0,
+            var styleIcon: Int = 0,
+            var bigPicture: Int = 0,
+            var extender: Int = 0,
+            var extras: Int = 0,
+        )
+
+        val totals =
+            memoryUse.fold(Totals()) { t, usage ->
+                t.smallIcon += usage.objectUsage.smallIcon
+                t.largeIcon += usage.objectUsage.largeIcon
+                t.styleIcon += usage.objectUsage.styleIcon
+                t.bigPicture += usage.objectUsage.bigPicture
+                t.extender += usage.objectUsage.extender
+                t.extras += usage.objectUsage.extras
+                t
+            }
+
+        pw.println()
+        pw.println("TOTALS")
+        pw.println(
+            "".padEnd(35) +
+                "\t\t" +
+                "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" +
+                "".padEnd(15) +
+                "\t\t${toKb(totals.styleIcon)}\t" +
+                "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" +
+                toKb(totals.extras)
+        )
+        pw.println()
     }
 
-    /**
-     * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
-     * bitmaps). Can be slow.
-     */
-    private fun computeBundleSize(extras: Bundle): Int {
-        val parcel = Parcel.obtain()
-        try {
-            extras.writeToParcel(parcel, 0)
-            return parcel.dataSize()
-        } finally {
-            parcel.recycle()
-        }
+    /** Renders a table of notification view usage into passed [PrintWriter] */
+    private fun dumpNotificationViewUsage(
+        pw: PrintWriter,
+        memoryUse: List<NotificationMemoryUsage>,
+    ) {
+
+        data class Totals(
+            var smallIcon: Int = 0,
+            var largeIcon: Int = 0,
+            var style: Int = 0,
+            var customViews: Int = 0,
+            var softwareBitmapsPenalty: Int = 0,
+        )
+
+        val totals = Totals()
+        pw.println("Notification View Usage")
+        pw.println("-----------")
+        pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware")
+        pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps")
+        pw.println()
+        memoryUse
+            .filter { it.viewUsage.isNotEmpty() }
+            .forEach { use ->
+                pw.println(use.packageName + " " + use.notificationKey)
+                use.viewUsage.forEach { view ->
+                    pw.println(
+                        "  ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" +
+                            "\t${view.largeIcon}\t${view.style}" +
+                            "\t${view.customViews}\t${view.softwareBitmapsPenalty}"
+                    )
+
+                    if (view.viewType == ViewType.TOTAL) {
+                        totals.smallIcon += view.smallIcon
+                        totals.largeIcon += view.largeIcon
+                        totals.style += view.style
+                        totals.customViews += view.customViews
+                        totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty
+                    }
+                }
+            }
+        pw.println()
+        pw.println("TOTALS")
+        pw.println(
+            "  ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" +
+                "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" +
+                "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}"
+        )
+        pw.println()
     }
 
-    /**
-     * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
-     * if the key does not exist in extras.
-     */
-    private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
-        return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
-            is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
-            is Icon -> computeIconUse(parcelable, seenBitmaps)
-            is Person -> computeIconUse(parcelable.icon, seenBitmaps)
-            else -> 0
-        }
-    }
-
-    /**
-     * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
-     * defined via Uri or a resource.
-     *
-     * @return memory usage in bytes or 0 if the icon is Uri/Resource based
-     */
-    private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) =
-        when (icon?.type) {
-            Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
-            Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
-            Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
-            else -> 0
-        }
-
-    /**
-     * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
-     * seenBitmaps set, this method returns 0 to avoid double counting.
-     *
-     * @return memory usage of the bitmap in bytes
-     */
-    private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
-        val refId = System.identityHashCode(bitmap)
-        if (seenBitmaps?.contains(refId) == true) {
-            return 0
-        }
-
-        seenBitmaps?.add(refId)
-        return bitmap.allocationByteCount
-    }
-
-    private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
-        val refId = System.identityHashCode(icon.dataBytes)
-        if (seenBitmaps.contains(refId)) {
-            return 0
-        }
-
-        seenBitmaps.add(refId)
-        return icon.dataLength
+    private fun toKb(bytes: Int): String {
+        return (bytes / 1024).toString() + " KB"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
new file mode 100644
index 0000000..a0bee15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -0,0 +1,173 @@
+package com.android.systemui.statusbar.notification.logging
+
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import com.android.internal.R
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.util.children
+
+/** Walks view hiearchy of a given notification to estimate its memory use. */
+internal object NotificationMemoryViewWalker {
+
+    private const val TAG = "NotificationMemory"
+
+    /** Builder for [NotificationViewUsage] objects. */
+    private class UsageBuilder {
+        private var smallIcon: Int = 0
+        private var largeIcon: Int = 0
+        private var systemIcons: Int = 0
+        private var style: Int = 0
+        private var customViews: Int = 0
+        private var softwareBitmaps = 0
+
+        fun addSmallIcon(smallIconUse: Int) = apply { smallIcon += smallIconUse }
+        fun addLargeIcon(largeIconUse: Int) = apply { largeIcon += largeIconUse }
+        fun addSystem(systemIconUse: Int) = apply { systemIcons += systemIconUse }
+        fun addStyle(styleUse: Int) = apply { style += styleUse }
+        fun addSoftwareBitmapPenalty(softwareBitmapUse: Int) = apply {
+            softwareBitmaps += softwareBitmapUse
+        }
+
+        fun addCustomViews(customViewsUse: Int) = apply { customViews += customViewsUse }
+
+        fun build(viewType: ViewType): NotificationViewUsage {
+            return NotificationViewUsage(
+                viewType = viewType,
+                smallIcon = smallIcon,
+                largeIcon = largeIcon,
+                systemIcons = systemIcons,
+                style = style,
+                customViews = customViews,
+                softwareBitmapsPenalty = softwareBitmaps,
+            )
+        }
+    }
+
+    /**
+     * Returns memory usage of public and private views contained in passed
+     * [ExpandableNotificationRow]
+     */
+    fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> {
+        if (row == null) {
+            return listOf()
+        }
+
+        // The ordering here is significant since it determines deduplication of seen drawables.
+        return listOf(
+            getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
+            getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild),
+            getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
+            getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout),
+            getTotalUsage(row)
+        )
+    }
+
+    /**
+     * Calculate total usage of all views - we need to do a separate traversal to make sure we don't
+     * double count fields.
+     */
+    private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage {
+        val totalUsage = UsageBuilder()
+        val seenObjects = hashSetOf<Int>()
+
+        row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) }
+        row.privateLayout?.let { child ->
+            for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) {
+                (view as? ViewGroup)?.let { v ->
+                    computeViewHierarchyUse(v, totalUsage, seenObjects)
+                }
+            }
+        }
+        return totalUsage.build(ViewType.TOTAL)
+    }
+
+    private fun getViewUsage(
+        type: ViewType,
+        rootView: View?,
+        seenObjects: HashSet<Int> = hashSetOf()
+    ): NotificationViewUsage {
+        val usageBuilder = UsageBuilder()
+        (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) }
+        return usageBuilder.build(type)
+    }
+
+    private fun computeViewHierarchyUse(
+        rootView: ViewGroup,
+        builder: UsageBuilder,
+        seenObjects: HashSet<Int> = hashSetOf(),
+    ) {
+        for (child in rootView.children) {
+            if (child is ViewGroup) {
+                computeViewHierarchyUse(child, builder, seenObjects)
+            } else {
+                computeViewUse(child, builder, seenObjects)
+            }
+        }
+    }
+
+    private fun computeViewUse(view: View, builder: UsageBuilder, seenObjects: HashSet<Int>) {
+        if (view !is ImageView) return
+        val drawable = view.drawable ?: return
+        val drawableRef = System.identityHashCode(drawable)
+        if (seenObjects.contains(drawableRef)) return
+        val drawableUse = computeDrawableUse(drawable, seenObjects)
+        // TODO(b/235451049): We need to make sure we traverse large icon before small icon -
+        // sometimes the large icons are assigned to small icon views and we want to
+        // attribute them to large view in those cases.
+        when (view.id) {
+            R.id.left_icon,
+            R.id.icon,
+            R.id.conversation_icon -> builder.addSmallIcon(drawableUse)
+            R.id.right_icon -> builder.addLargeIcon(drawableUse)
+            R.id.big_picture -> builder.addStyle(drawableUse)
+            // Elements that are part of platform with resources
+            R.id.phishing_alert,
+            R.id.feedback,
+            R.id.alerted_icon,
+            R.id.expand_button_icon,
+            R.id.remote_input_send -> builder.addSystem(drawableUse)
+            // Custom view ImageViews
+            else -> {
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Custom view: ${identifierForView(view)}")
+                }
+                builder.addCustomViews(drawableUse)
+            }
+        }
+
+        if (isDrawableSoftwareBitmap(drawable)) {
+            builder.addSoftwareBitmapPenalty(drawableUse)
+        }
+
+        seenObjects.add(drawableRef)
+    }
+
+    private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int =
+        when (drawable) {
+            is BitmapDrawable -> {
+                val ref = System.identityHashCode(drawable.bitmap)
+                if (seenObjects.contains(ref)) {
+                    0
+                } else {
+                    seenObjects.add(ref)
+                    drawable.bitmap.allocationByteCount
+                }
+            }
+            else -> 0
+        }
+
+    private fun isDrawableSoftwareBitmap(drawable: Drawable) =
+        drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE
+
+    private fun identifierForView(view: View) =
+        if (view.id == View.NO_ID) {
+            "no-id"
+        } else {
+            view.resources.getResourceName(view.id)
+        }
+}
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 755e3e1..d29298a 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
@@ -613,22 +613,21 @@
     protected void resetAllContentAlphas() {}
 
     @Override
-    protected void applyRoundness() {
+    public void applyRoundness() {
         super.applyRoundness();
-        applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
-                getCurrentBackgroundRadiusBottom());
+        applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
     }
 
     @Override
-    public float getCurrentBackgroundRadiusTop() {
+    public float getTopCornerRadius() {
         float fraction = getInterpolatedAppearAnimationFraction();
-        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction);
+        return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
     }
 
     @Override
-    public float getCurrentBackgroundRadiusBottom() {
+    public float getBottomCornerRadius() {
         float fraction = getInterpolatedAppearAnimationFraction();
-        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction);
+        return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
     }
 
     private void applyBackgroundRoundness(float topRadius, float bottomRadius) {
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 087dc71..9e7717c 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
@@ -93,6 +93,7 @@
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -154,7 +155,9 @@
         void onLayout();
     }
 
-    /** Listens for changes to the expansion state of this row. */
+    /**
+     * Listens for changes to the expansion state of this row.
+     */
     public interface OnExpansionChangedListener {
         void onExpansionChanged(boolean isExpanded);
     }
@@ -183,22 +186,34 @@
     private int mNotificationLaunchHeight;
     private boolean mMustStayOnScreen;
 
-    /** Does this row contain layouts that can adapt to row expansion */
+    /**
+     * Does this row contain layouts that can adapt to row expansion
+     */
     private boolean mExpandable;
-    /** Has the user actively changed the expansion state of this row */
+    /**
+     * Has the user actively changed the expansion state of this row
+     */
     private boolean mHasUserChangedExpansion;
-    /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
+    /**
+     * If {@link #mHasUserChangedExpansion}, has the user expanded this row
+     */
     private boolean mUserExpanded;
-    /** Whether the blocking helper is showing on this notification (even if dismissed) */
+    /**
+     * Whether the blocking helper is showing on this notification (even if dismissed)
+     */
     private boolean mIsBlockingHelperShowing;
 
     /**
      * Has this notification been expanded while it was pinned
      */
     private boolean mExpandedWhenPinned;
-    /** Is the user touching this row */
+    /**
+     * Is the user touching this row
+     */
     private boolean mUserLocked;
-    /** Are we showing the "public" version */
+    /**
+     * Are we showing the "public" version
+     */
     private boolean mShowingPublic;
     private boolean mSensitive;
     private boolean mSensitiveHiddenInGeneral;
@@ -351,11 +366,14 @@
     private boolean mWasChildInGroupWhenRemoved;
     private NotificationInlineImageResolver mImageResolver;
     private NotificationMediaManager mMediaManager;
-    @Nullable private OnExpansionChangedListener mExpansionChangedListener;
-    @Nullable private Runnable mOnIntrinsicHeightReachedRunnable;
+    @Nullable
+    private OnExpansionChangedListener mExpansionChangedListener;
+    @Nullable
+    private Runnable mOnIntrinsicHeightReachedRunnable;
 
     private float mTopRoundnessDuringLaunchAnimation;
     private float mBottomRoundnessDuringLaunchAnimation;
+    private boolean mIsNotificationGroupCornerEnabled;
 
     /**
      * Returns whether the given {@code statusBarNotification} is a system notification.
@@ -574,14 +592,18 @@
         }
     }
 
-    /** Called when the notification's ranking was changed (but nothing else changed). */
+    /**
+     * Called when the notification's ranking was changed (but nothing else changed).
+     */
     public void onNotificationRankingUpdated() {
         if (mMenuRow != null) {
             mMenuRow.onNotificationUpdated(mEntry.getSbn());
         }
     }
 
-    /** Call when bubble state has changed and the button on the notification should be updated. */
+    /**
+     * Call when bubble state has changed and the button on the notification should be updated.
+     */
     public void updateBubbleButton() {
         for (NotificationContentView l : mLayouts) {
             l.updateBubbleButton(mEntry);
@@ -620,6 +642,7 @@
 
     /**
      * Sets a supplier that can determine whether the keyguard is secure or not.
+     *
      * @param secureStateProvider A function that returns true if keyguard is secure.
      */
     public void setSecureStateProvider(BooleanSupplier secureStateProvider) {
@@ -781,7 +804,9 @@
         mChildrenContainer.setUntruncatedChildCount(childCount);
     }
 
-    /** Called after children have been attached to set the expansion states */
+    /**
+     * Called after children have been attached to set the expansion states
+     */
     public void resetChildSystemExpandedStates() {
         if (isSummaryWithChildren()) {
             mChildrenContainer.updateExpansionStates();
@@ -791,7 +816,7 @@
     /**
      * Add a child notification to this view.
      *
-     * @param row the row to add
+     * @param row        the row to add
      * @param childIndex the index to add it at, if -1 it will be added at the end
      */
     public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
@@ -809,10 +834,12 @@
         }
         onAttachedChildrenCountChanged();
         row.setIsChildInGroup(false, null);
-        row.setBottomRoundness(0.0f, false /* animate */);
+        row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
     }
 
-    /** Returns the child notification at [index], or null if no such child. */
+    /**
+     * Returns the child notification at [index], or null if no such child.
+     */
     @Nullable
     public ExpandableNotificationRow getChildNotificationAt(int index) {
         if (mChildrenContainer == null
@@ -834,7 +861,7 @@
 
     /**
      * @param isChildInGroup Is this notification now in a group
-     * @param parent the new parent notification
+     * @param parent         the new parent notification
      */
     public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {
         if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) {
@@ -898,7 +925,9 @@
         return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren();
     }
 
-    /** Updates states of all children. */
+    /**
+     * Updates states of all children.
+     */
     public void updateChildrenStates(AmbientState ambientState) {
         if (mIsSummaryWithChildren) {
             ExpandableViewState parentState = getViewState();
@@ -906,21 +935,27 @@
         }
     }
 
-    /** Applies children states. */
+    /**
+     * Applies children states.
+     */
     public void applyChildrenState() {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.applyState();
         }
     }
 
-    /** Prepares expansion changed. */
+    /**
+     * Prepares expansion changed.
+     */
     public void prepareExpansionChanged() {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.prepareExpansionChanged();
         }
     }
 
-    /** Starts child animations. */
+    /**
+     * Starts child animations.
+     */
     public void startChildAnimation(AnimationProperties properties) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.startAnimationToState(properties);
@@ -984,7 +1019,7 @@
         if (mIsSummaryWithChildren) {
             return mChildrenContainer.getIntrinsicHeight();
         }
-        if(mExpandedWhenPinned) {
+        if (mExpandedWhenPinned) {
             return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
         } else if (atLeastMinHeight) {
             return Math.max(getCollapsedHeight(), getHeadsUpHeight());
@@ -1079,18 +1114,22 @@
         updateClickAndFocus();
     }
 
-    /** The click listener for the bubble button. */
+    /**
+     * The click listener for the bubble button.
+     */
     public View.OnClickListener getBubbleClickListener() {
         return v -> {
             if (mBubblesManagerOptional.isPresent()) {
                 mBubblesManagerOptional.get()
-                    .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
+                        .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
             }
             mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
         };
     }
 
-    /** The click listener for the snooze button. */
+    /**
+     * The click listener for the snooze button.
+     */
     public View.OnClickListener getSnoozeClickListener(MenuItem item) {
         return v -> {
             // Dismiss a snoozed notification if one is still left behind
@@ -1252,7 +1291,7 @@
     }
 
     public void setContentBackground(int customBackgroundColor, boolean animate,
-            NotificationContentView notificationContentView) {
+                                     NotificationContentView notificationContentView) {
         if (getShowingLayout() == notificationContentView) {
             setTintColor(customBackgroundColor, animate);
         }
@@ -1637,7 +1676,9 @@
         setTargetPoint(null);
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.setFeedbackIcon(icon);
@@ -1646,7 +1687,9 @@
         mPublicLayout.setFeedbackIcon(icon);
     }
 
-    /** Sets the last time the notification being displayed audibly alerted the user. */
+    /**
+     * Sets the last time the notification being displayed audibly alerted the user.
+     */
     public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) {
         long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs;
         boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS;
@@ -1700,7 +1743,9 @@
         Trace.endSection();
     }
 
-    /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */
+    /**
+     * Generates and appends "(MessagingStyle)" type tag to passed string for tracing.
+     */
     @NonNull
     private String appendTraceStyleTag(@NonNull String traceTag) {
         if (!Trace.isEnabled()) {
@@ -1721,7 +1766,7 @@
         super.onFinishInflate();
         mPublicLayout = findViewById(R.id.expandedPublic);
         mPrivateLayout = findViewById(R.id.expanded);
-        mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+        mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
 
         for (NotificationContentView l : mLayouts) {
             l.setExpandClickListener(mExpandClickListener);
@@ -1740,6 +1785,7 @@
             mChildrenContainer.setIsLowPriority(mIsLowPriority);
             mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
             mChildrenContainer.onNotificationUpdated();
+            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
 
             mTranslateableViews.add(mChildrenContainer);
         });
@@ -1796,6 +1842,7 @@
     /**
      * Perform a smart action which triggers a longpress (expose guts).
      * Based on the semanticAction passed, may update the state of the guts view.
+     *
      * @param semanticAction associated with this smart action click
      */
     public void doSmartActionClick(int x, int y, int semanticAction) {
@@ -1939,9 +1986,10 @@
 
     /**
      * Set the dismiss behavior of the view.
+     *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
-     *                                          translationX, otherwise the contents will be
-     *                                          translated.
+     *                             translationX, otherwise the contents will be
+     *                             translated.
      */
     @Override
     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
@@ -1955,6 +2003,14 @@
             if (previousTranslation != 0) {
                 setTranslation(previousTranslation);
             }
+            if (mChildrenContainer != null) {
+                List<ExpandableNotificationRow> notificationChildren =
+                        mChildrenContainer.getAttachedChildren();
+                for (int i = 0; i < notificationChildren.size(); i++) {
+                    ExpandableNotificationRow child = notificationChildren.get(i);
+                    child.setDismissUsingRowTranslationX(usingRowTranslationX);
+                }
+            }
         }
     }
 
@@ -2009,7 +2065,7 @@
     }
 
     public Animator getTranslateViewAnimator(final float leftTarget,
-            AnimatorUpdateListener listener) {
+                                             AnimatorUpdateListener listener) {
         if (mTranslateAnim != null) {
             mTranslateAnim.cancel();
         }
@@ -2115,7 +2171,7 @@
                             NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
             float startTop = params.getStartNotificationTop();
             top = (int) Math.min(MathUtils.lerp(startTop,
-                    params.getTop(), expandProgress),
+                            params.getTop(), expandProgress),
                     startTop);
         } else {
             top = params.getTop();
@@ -2151,29 +2207,30 @@
         }
         setTranslationY(top);
 
-        mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius;
-        mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius;
+        final float maxRadius = getMaxRadius();
+        mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / maxRadius;
+        mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / maxRadius;
         invalidateOutline();
 
         mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight);
     }
 
     @Override
-    public float getCurrentTopRoundness() {
+    public float getTopRoundness() {
         if (mExpandAnimationRunning) {
             return mTopRoundnessDuringLaunchAnimation;
         }
 
-        return super.getCurrentTopRoundness();
+        return super.getTopRoundness();
     }
 
     @Override
-    public float getCurrentBottomRoundness() {
+    public float getBottomRoundness() {
         if (mExpandAnimationRunning) {
             return mBottomRoundnessDuringLaunchAnimation;
         }
 
-        return super.getCurrentBottomRoundness();
+        return super.getBottomRoundness();
     }
 
     public void setExpandAnimationRunning(boolean expandAnimationRunning) {
@@ -2284,7 +2341,7 @@
     /**
      * Set this notification to be expanded by the user
      *
-     * @param userExpanded whether the user wants this notification to be expanded
+     * @param userExpanded        whether the user wants this notification to be expanded
      * @param allowChildExpansion whether a call to this method allows expanding children
      */
     public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
@@ -2434,7 +2491,7 @@
 
     /**
      * @return {@code true} if the notification can show it's heads up layout. This is mostly true
-     *         except for legacy use cases.
+     * except for legacy use cases.
      */
     public boolean canShowHeadsUp() {
         if (mOnKeyguard && !isDozing() && !isBypassEnabled()) {
@@ -2625,7 +2682,7 @@
 
     @Override
     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
-            long duration) {
+                                 long duration) {
         if (getVisibility() == GONE) {
             // If we are GONE, the hideSensitive parameter will not be calculated and always be
             // false, which is incorrect, let's wait until a real call comes in later.
@@ -2658,9 +2715,9 @@
 
     private void animateShowingPublic(long delay, long duration, boolean showingPublic) {
         View[] privateViews = mIsSummaryWithChildren
-                ? new View[] {mChildrenContainer}
-                : new View[] {mPrivateLayout};
-        View[] publicViews = new View[] {mPublicLayout};
+                ? new View[]{mChildrenContainer}
+                : new View[]{mPrivateLayout};
+        View[] publicViews = new View[]{mPublicLayout};
         View[] hiddenChildren = showingPublic ? privateViews : publicViews;
         View[] shownChildren = showingPublic ? publicViews : privateViews;
         for (final View hiddenView : hiddenChildren) {
@@ -2693,8 +2750,8 @@
 
     /**
      * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
-     *         otherwise some state might not be updated. To request about the general clearability
-     *         see {@link NotificationEntry#isDismissable()}.
+     * otherwise some state might not be updated. To request about the general clearability
+     * see {@link NotificationEntry#isDismissable()}.
      */
     public boolean canViewBeDismissed() {
         return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
@@ -2777,8 +2834,13 @@
     }
 
     @Override
-    public long performRemoveAnimation(long duration, long delay, float translationDirection,
-            boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable,
+    public long performRemoveAnimation(
+            long duration,
+            long delay,
+            float translationDirection,
+            boolean isHeadsUpAnimation,
+            float endLocation,
+            Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener) {
         if (mMenuRow != null && mMenuRow.isMenuVisible()) {
             Animator anim = getTranslateViewAnimator(0f, null /* listener */);
@@ -2828,7 +2890,9 @@
         }
     }
 
-    /** Gets the last value set with {@link #setNotificationFaded(boolean)} */
+    /**
+     * Gets the last value set with {@link #setNotificationFaded(boolean)}
+     */
     @Override
     public boolean isNotificationFaded() {
         return mIsFaded;
@@ -2843,7 +2907,7 @@
      * notifications return false from {@link #hasOverlappingRendering()} and delegate the
      * layerType to child views which really need it in order to render correctly, such as icon
      * views or the conversation face pile.
-     *
+     * <p>
      * Another compounding factor for notifications is that we change clipping on each frame of the
      * animation, so the hardware layer isn't able to do any caching at the top level, but the
      * individual elements we render with hardware layers (e.g. icons) cache wonderfully because we
@@ -2869,7 +2933,9 @@
         }
     }
 
-    /** Private helper for iterating over the layouts and children containers to set faded state */
+    /**
+     * Private helper for iterating over the layouts and children containers to set faded state
+     */
     private void setNotificationFadedOnChildren(boolean faded) {
         delegateNotificationFaded(mChildrenContainer, faded);
         for (NotificationContentView layout : mLayouts) {
@@ -2897,7 +2963,7 @@
      * Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the
      * row should require overlapping rendering to ensure that the overlapped view doesn't bleed
      * through when alpha fading.
-     *
+     * <p>
      * Note that this currently works for top-level notifications which squish their height down
      * while collapsing the shade, but does not work for children inside groups, because the
      * accordion affect does not apply to those views, so super.hasOverlappingRendering() will
@@ -2976,7 +3042,7 @@
             return mGuts.getIntrinsicHeight();
         } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp
                 && mHeadsUpManager.isTrackingHeadsUp()) {
-                return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+            return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
         } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) {
             return mChildrenContainer.getMinHeight();
         } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) {
@@ -3218,8 +3284,8 @@
             MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext());
             if (snoozeMenu != null) {
                 AccessibilityAction action = new AccessibilityAction(R.id.action_snooze,
-                    getContext().getResources()
-                        .getString(R.string.notification_menu_snooze_action));
+                        getContext().getResources()
+                                .getString(R.string.notification_menu_snooze_action));
                 info.addAction(action);
             }
         }
@@ -3280,17 +3346,17 @@
             NotificationContentView contentView = (NotificationContentView) child;
             if (isClippingNeeded()) {
                 return true;
-            } else if (!hasNoRounding()
-                    && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f,
-                    getCurrentBottomRoundness() != 0.0f)) {
+            } else if (hasRoundedCorner()
+                    && contentView.shouldClipToRounding(getTopRoundness() != 0.0f,
+                    getBottomRoundness() != 0.0f)) {
                 return true;
             }
         } else if (child == mChildrenContainer) {
-            if (isClippingNeeded() || !hasNoRounding()) {
+            if (isClippingNeeded() || hasRoundedCorner()) {
                 return true;
             }
         } else if (child instanceof NotificationGuts) {
-            return !hasNoRounding();
+            return hasRoundedCorner();
         }
         return super.childNeedsClipping(child);
     }
@@ -3316,14 +3382,17 @@
     }
 
     @Override
-    protected void applyRoundness() {
+    public void applyRoundness() {
         super.applyRoundness();
         applyChildrenRoundness();
     }
 
     private void applyChildrenRoundness() {
         if (mIsSummaryWithChildren) {
-            mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness());
+            mChildrenContainer.requestBottomRoundness(
+                    getBottomRoundness(),
+                    /* animate = */ false,
+                    SourceType.DefaultValue);
         }
     }
 
@@ -3335,10 +3404,6 @@
         return super.getCustomClipPath(child);
     }
 
-    private boolean hasNoRounding() {
-        return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f;
-    }
-
     public boolean isMediaRow() {
         return mEntry.getSbn().getNotification().isMediaNotification();
     }
@@ -3434,6 +3499,7 @@
     public interface LongPressListener {
         /**
          * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
+         *
          * @return whether the longpress was handled
          */
         boolean onLongPress(View v, int x, int y, MenuItem item);
@@ -3455,6 +3521,7 @@
     public interface CoordinateOnClickListener {
         /**
          * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates
+         *
          * @return whether the click was handled
          */
         boolean onClick(View v, int x, int y, MenuItem item);
@@ -3511,7 +3578,19 @@
     private void setTargetPoint(Point p) {
         mTargetPoint = p;
     }
+
     public Point getTargetPoint() {
         return mTargetPoint;
     }
+
+    /**
+     * Enable the support for rounded corner in notification group
+     * @param enabled true if is supported
+     */
+    public void enableNotificationGroupCorner(boolean enabled) {
+        mIsNotificationGroupCornerEnabled = enabled;
+        if (mChildrenContainer != null) {
+            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a493a67..842526e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -231,6 +231,8 @@
                 mStatusBarStateController.removeCallback(mStatusBarStateListener);
             }
         });
+        mView.enableNotificationGroupCorner(
+                mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER));
     }
 
     private final StatusBarStateController.StateListener mStatusBarStateListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index d58fe3b..4fde5d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,46 +28,21 @@
 import android.view.ViewOutlineProvider;
 
 import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 
 /**
  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
  */
 public abstract class ExpandableOutlineView extends ExpandableView {
 
-    private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
-            "topRoundness",
-            ExpandableOutlineView::setTopRoundnessInternal,
-            ExpandableOutlineView::getCurrentTopRoundness,
-            R.id.top_roundess_animator_tag,
-            R.id.top_roundess_animator_end_tag,
-            R.id.top_roundess_animator_start_tag);
-    private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from(
-            "bottomRoundness",
-            ExpandableOutlineView::setBottomRoundnessInternal,
-            ExpandableOutlineView::getCurrentBottomRoundness,
-            R.id.bottom_roundess_animator_tag,
-            R.id.bottom_roundess_animator_end_tag,
-            R.id.bottom_roundess_animator_start_tag);
-    private static final AnimationProperties ROUNDNESS_PROPERTIES =
-            new AnimationProperties().setDuration(
-                    StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS);
+    private RoundableState mRoundableState;
     private static final Path EMPTY_PATH = new Path();
-
     private final Rect mOutlineRect = new Rect();
-    private final Path mClipPath = new Path();
     private boolean mCustomOutline;
     private float mOutlineAlpha = -1f;
-    protected float mOutlineRadius;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    private float mCurrentBottomRoundness;
-    private float mCurrentTopRoundness;
-    private float mBottomRoundness;
-    private float mTopRoundness;
     private int mBackgroundTop;
 
     /**
@@ -80,8 +55,7 @@
     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
         @Override
         public void getOutline(View view, Outline outline) {
-            if (!mCustomOutline && getCurrentTopRoundness() == 0.0f
-                    && getCurrentBottomRoundness() == 0.0f && !mAlwaysRoundBothCorners) {
+            if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) {
                 // Only when translating just the contents, does the outline need to be shifted.
                 int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0;
                 int left = Math.max(translation, 0);
@@ -99,14 +73,18 @@
         }
     };
 
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     protected Path getClipPath(boolean ignoreTranslation) {
         int left;
         int top;
         int right;
         int bottom;
         int height;
-        float topRoundness = mAlwaysRoundBothCorners
-                ? mOutlineRadius : getCurrentBackgroundRadiusTop();
+        float topRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius();
         if (!mCustomOutline) {
             // The outline just needs to be shifted if we're translating the contents. Otherwise
             // it's already in the right place.
@@ -130,12 +108,11 @@
         if (height == 0) {
             return EMPTY_PATH;
         }
-        float bottomRoundness = mAlwaysRoundBothCorners
-                ? mOutlineRadius : getCurrentBackgroundRadiusBottom();
+        float bottomRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
         if (topRoundness + bottomRoundness > height) {
             float overShoot = topRoundness + bottomRoundness - height;
-            float currentTopRoundness = getCurrentTopRoundness();
-            float currentBottomRoundness = getCurrentBottomRoundness();
+            float currentTopRoundness = getTopRoundness();
+            float currentBottomRoundness = getBottomRoundness();
             topRoundness -= overShoot * currentTopRoundness
                     / (currentTopRoundness + currentBottomRoundness);
             bottomRoundness -= overShoot * currentBottomRoundness
@@ -145,8 +122,18 @@
         return mTmpPath;
     }
 
-    public void getRoundedRectPath(int left, int top, int right, int bottom,
-            float topRoundness, float bottomRoundness, Path outPath) {
+    /**
+     * Add a round rect in {@code outPath}
+     * @param outPath destination path
+     */
+    public void getRoundedRectPath(
+            int left,
+            int top,
+            int right,
+            int bottom,
+            float topRoundness,
+            float bottomRoundness,
+            Path outPath) {
         outPath.reset();
         mTmpCornerRadii[0] = topRoundness;
         mTmpCornerRadii[1] = topRoundness;
@@ -168,15 +155,28 @@
     @Override
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
         canvas.save();
+        Path clipPath = null;
+        Path childClipPath = null;
         if (childNeedsClipping(child)) {
-            Path clipPath = getCustomClipPath(child);
+            clipPath = getCustomClipPath(child);
             if (clipPath == null) {
                 clipPath = getClipPath(false /* ignoreTranslation */);
             }
-            if (clipPath != null) {
-                canvas.clipPath(clipPath);
+            // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the
+            // children instead.
+            if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) {
+                childClipPath = clipPath;
+                clipPath = null;
             }
         }
+
+        if (child instanceof NotificationChildrenContainer) {
+            ((NotificationChildrenContainer) child).setChildClipPath(childClipPath);
+        }
+        if (clipPath != null) {
+            canvas.clipPath(clipPath);
+        }
+
         boolean result = super.drawChild(canvas, child, drawingTime);
         canvas.restore();
         return result;
@@ -207,73 +207,21 @@
 
     private void initDimens() {
         Resources res = getResources();
-        mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
         mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
-        if (!mAlwaysRoundBothCorners) {
-            mOutlineRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
+        float maxRadius;
+        if (mAlwaysRoundBothCorners) {
+            maxRadius = res.getDimension(R.dimen.notification_shadow_radius);
+        } else {
+            maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
         }
+        mRoundableState = new RoundableState(this, this, maxRadius);
         setClipToOutline(mAlwaysRoundBothCorners);
     }
 
     @Override
-    public boolean setTopRoundness(float topRoundness, boolean animate) {
-        if (mTopRoundness != topRoundness) {
-            float diff = Math.abs(topRoundness - mTopRoundness);
-            mTopRoundness = topRoundness;
-            boolean shouldAnimate = animate;
-            if (PropertyAnimator.isAnimating(this, TOP_ROUNDNESS) && diff > 0.5f) {
-                // Fail safe:
-                // when we've been animating previously and we're now getting an update in the
-                // other direction, make sure to animate it too, otherwise, the localized updating
-                // may make the start larger than 1.0.
-                shouldAnimate = true;
-            }
-            PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness,
-                    ROUNDNESS_PROPERTIES, shouldAnimate);
-            return true;
-        }
-        return false;
-    }
-
-    protected void applyRoundness() {
+    public void applyRoundness() {
         invalidateOutline();
-        invalidate();
-    }
-
-    public float getCurrentBackgroundRadiusTop() {
-        return getCurrentTopRoundness() * mOutlineRadius;
-    }
-
-    public float getCurrentTopRoundness() {
-        return mCurrentTopRoundness;
-    }
-
-    public float getCurrentBottomRoundness() {
-        return mCurrentBottomRoundness;
-    }
-
-    public float getCurrentBackgroundRadiusBottom() {
-        return getCurrentBottomRoundness() * mOutlineRadius;
-    }
-
-    @Override
-    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
-        if (mBottomRoundness != bottomRoundness) {
-            float diff = Math.abs(bottomRoundness - mBottomRoundness);
-            mBottomRoundness = bottomRoundness;
-            boolean shouldAnimate = animate;
-            if (PropertyAnimator.isAnimating(this, BOTTOM_ROUNDNESS) && diff > 0.5f) {
-                // Fail safe:
-                // when we've been animating previously and we're now getting an update in the
-                // other direction, make sure to animate it too, otherwise, the localized updating
-                // may make the start larger than 1.0.
-                shouldAnimate = true;
-            }
-            PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness,
-                    ROUNDNESS_PROPERTIES, shouldAnimate);
-            return true;
-        }
-        return false;
+        super.applyRoundness();
     }
 
     protected void setBackgroundTop(int backgroundTop) {
@@ -283,16 +231,6 @@
         }
     }
 
-    private void setTopRoundnessInternal(float topRoundness) {
-        mCurrentTopRoundness = topRoundness;
-        applyRoundness();
-    }
-
-    private void setBottomRoundnessInternal(float bottomRoundness) {
-        mCurrentBottomRoundness = bottomRoundness;
-        applyRoundness();
-    }
-
     public void onDensityOrFontScaleChanged() {
         initDimens();
         applyRoundness();
@@ -348,9 +286,10 @@
 
     /**
      * Set the dismiss behavior of the view.
+     *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
-     *                                          translationX, otherwise the contents will be
-     *                                          translated.
+     *                             translationX, otherwise the contents will be
+     *                             translated.
      */
     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
         mDismissUsingRowTranslationX = usingRowTranslationX;
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 38f0c55..955d7c1 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
@@ -36,6 +36,8 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.util.DumpUtilsKt;
@@ -47,9 +49,10 @@
 /**
  * An abstract view for expandable views.
  */
-public abstract class ExpandableView extends FrameLayout implements Dumpable {
+public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
     private static final String TAG = "ExpandableView";
 
+    private RoundableState mRoundableState = null;
     protected OnHeightChangedListener mOnHeightChangedListener;
     private int mActualHeight;
     protected int mClipTopAmount;
@@ -78,6 +81,14 @@
         initDimens();
     }
 
+    @Override
+    public RoundableState getRoundableState() {
+        if (mRoundableState == null) {
+            mRoundableState = new RoundableState(this, this, 0f);
+        }
+        return mRoundableState;
+    }
+
     private void initDimens() {
         mContentShift = getResources().getDimensionPixelSize(
                 R.dimen.shelf_transform_content_shift);
@@ -440,8 +451,7 @@
             int top = getClipTopAmount();
             int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding()
                     - mClipBottomAmount, top), mMinimumHeightForClipping);
-            int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f);
-            mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom);
+            mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
             setClipBounds(mClipRect);
         } else {
             setClipBounds(null);
@@ -455,7 +465,6 @@
 
     public void setExtraWidthForClipping(float extraWidthForClipping) {
         mExtraWidthForClipping = extraWidthForClipping;
-        updateClipping();
     }
 
     public float getHeaderVisibleAmount() {
@@ -844,22 +853,6 @@
         return mFirstInSection;
     }
 
-    /**
-     * Set the topRoundness of this view.
-     * @return Whether the roundness was changed.
-     */
-    public boolean setTopRoundness(float topRoundness, boolean animate) {
-        return false;
-    }
-
-    /**
-     * Set the bottom roundness of this view.
-     * @return Whether the roundness was changed.
-     */
-    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
-        return false;
-    }
-
     public int getHeadsUpHeightWithoutHeader() {
         return getHeight();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 4c69304..c534860 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -40,7 +40,7 @@
 import com.android.internal.widget.ImageMessageConsumer;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 8de0365..277ad8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1374,13 +1374,8 @@
         if (bubbleButton == null || actionContainer == null) {
             return;
         }
-        boolean isPersonWithShortcut =
-                mPeopleIdentifier.getPeopleNotificationType(entry)
-                        >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
-        boolean showButton = BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
-                && isPersonWithShortcut
-                && entry.getBubbleMetadata() != null;
-        if (showButton) {
+
+        if (shouldShowBubbleButton(entry)) {
             // explicitly resolve drawable resource using SystemUI's theme
             Drawable d = mContext.getDrawable(entry.isBubble()
                     ? R.drawable.bubble_ic_stop_bubble
@@ -1410,6 +1405,16 @@
         }
     }
 
+    @VisibleForTesting
+    boolean shouldShowBubbleButton(NotificationEntry entry) {
+        boolean isPersonWithShortcut =
+                mPeopleIdentifier.getPeopleNotificationType(entry)
+                        >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
+        return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+                && isPersonWithShortcut
+                && entry.getBubbleMetadata() != null;
+    }
+
     private void applySnoozeAction(View layout) {
         if (layout == null || mContainingNotification == null) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 7a65436..f13e48d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -35,12 +35,15 @@
 
 import com.android.internal.widget.CachingIconView;
 import com.android.internal.widget.NotificationExpandButton;
+import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.TransformableView;
 import com.android.systemui.statusbar.ViewTransformationHelper;
 import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.ImageTransformState;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.TransformState;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
@@ -49,13 +52,12 @@
 /**
  * Wraps a notification view which may or may not include a header.
  */
-public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
+public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable {
 
+    private final RoundableState mRoundableState;
     private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
             = new PathInterpolator(0.4f, 0f, 0.7f, 1f);
-
     protected final ViewTransformationHelper mTransformationHelper;
-
     private CachingIconView mIcon;
     private NotificationExpandButton mExpandButton;
     private View mAltExpandTarget;
@@ -67,12 +69,16 @@
     private ImageView mWorkProfileImage;
     private View mAudiblyAlertedIcon;
     private View mFeedbackIcon;
-
     private boolean mIsLowPriority;
     private boolean mTransformLowPriorityTitle;
 
     protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
         super(ctx, view, row);
+        mRoundableState = new RoundableState(
+                mView,
+                this,
+                ctx.getResources().getDimension(R.dimen.notification_corner_radius)
+        );
         mTransformationHelper = new ViewTransformationHelper();
 
         // we want to avoid that the header clashes with the other text when transforming
@@ -81,7 +87,8 @@
                 new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
 
                     @Override
-                    public Interpolator getCustomInterpolator(int interpolationType,
+                    public Interpolator getCustomInterpolator(
+                            int interpolationType,
                             boolean isFrom) {
                         boolean isLowPriority = mView instanceof NotificationHeaderView;
                         if (interpolationType == TRANSFORM_Y) {
@@ -99,11 +106,17 @@
                     protected boolean hasCustomTransformation() {
                         return mIsLowPriority && mTransformLowPriorityTitle;
                     }
-                }, TRANSFORMING_VIEW_TITLE);
+                },
+                TRANSFORMING_VIEW_TITLE);
         resolveHeaderViews();
         addFeedbackOnClickListener(row);
     }
 
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     protected void resolveHeaderViews() {
         mIcon = mView.findViewById(com.android.internal.R.id.icon);
         mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
@@ -128,7 +141,9 @@
         }
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     @Override
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mFeedbackIcon != null) {
@@ -193,7 +208,7 @@
                     // its animation
                     && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) {
                 ((ImageView) child).setCropToPadding(true);
-            } else if (child instanceof ViewGroup){
+            } else if (child instanceof ViewGroup) {
                 ViewGroup group = (ViewGroup) child;
                 for (int i = 0; i < group.getChildCount(); i++) {
                     stack.push(group.getChildAt(i));
@@ -215,7 +230,9 @@
     }
 
     @Override
-    public void updateExpandability(boolean expandable, View.OnClickListener onClickListener,
+    public void updateExpandability(
+            boolean expandable,
+            View.OnClickListener onClickListener,
             boolean requestLayout) {
         mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
         mExpandButton.setOnClickListener(expandable ? onClickListener : null);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..26f0ad9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -21,6 +21,9 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
 import android.graphics.drawable.ColorDrawable;
 import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
@@ -33,6 +36,7 @@
 import android.widget.RemoteViews;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -43,10 +47,14 @@
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.HybridGroupManager;
 import com.android.systemui.statusbar.notification.row.HybridNotificationView;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
 
 import java.util.ArrayList;
@@ -56,7 +64,7 @@
  * A container containing child notifications
  */
 public class NotificationChildrenContainer extends ViewGroup
-        implements NotificationFadeAware {
+        implements NotificationFadeAware, Roundable {
 
     private static final String TAG = "NotificationChildrenContainer";
 
@@ -100,9 +108,9 @@
     private boolean mEnableShadowOnChildNotifications;
 
     private NotificationHeaderView mNotificationHeader;
-    private NotificationViewWrapper mNotificationHeaderWrapper;
+    private NotificationHeaderViewWrapper mNotificationHeaderWrapper;
     private NotificationHeaderView mNotificationHeaderLowPriority;
-    private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
+    private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
     private NotificationGroupingUtil mGroupingUtil;
     private ViewState mHeaderViewState;
     private int mClipBottomAmount;
@@ -110,7 +118,8 @@
     private OnClickListener mHeaderClickListener;
     private ViewGroup mCurrentHeader;
     private boolean mIsConversation;
-
+    private Path mChildClipPath = null;
+    private final Path mHeaderPath = new Path();
     private boolean mShowGroupCountInExpander;
     private boolean mShowDividersWhenExpanded;
     private boolean mHideDividersDuringExpand;
@@ -119,6 +128,8 @@
     private float mHeaderVisibleAmount = 1.0f;
     private int mUntruncatedChildCount;
     private boolean mContainingNotificationIsFaded = false;
+    private RoundableState mRoundableState;
+    private boolean mIsNotificationGroupCornerEnabled;
 
     public NotificationChildrenContainer(Context context) {
         this(context, null);
@@ -132,10 +143,14 @@
         this(context, attrs, defStyleAttr, 0);
     }
 
-    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
+    public NotificationChildrenContainer(
+            Context context,
+            AttributeSet attrs,
+            int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         mHybridGroupManager = new HybridGroupManager(getContext());
+        mRoundableState = new RoundableState(this, this, 0f);
         initDimens();
         setClipChildren(false);
     }
@@ -167,6 +182,12 @@
         mHybridGroupManager.initDimens();
     }
 
+    @NonNull
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         int childCount =
@@ -271,7 +292,7 @@
     /**
      * Add a child notification to this view.
      *
-     * @param row the row to add
+     * @param row        the row to add
      * @param childIndex the index to add it at, if -1 it will be added at the end
      */
     public void addNotification(ExpandableNotificationRow row, int childIndex) {
@@ -347,8 +368,11 @@
             mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
                     .setVisibility(VISIBLE);
             mNotificationHeader.setOnClickListener(mHeaderClickListener);
-            mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
-                    mNotificationHeader, mContainingNotification);
+            mNotificationHeaderWrapper =
+                    (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+                            getContext(),
+                            mNotificationHeader,
+                            mContainingNotification);
             addView(mNotificationHeader, 0);
             invalidate();
         } else {
@@ -381,8 +405,11 @@
                 mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
                         .setVisibility(VISIBLE);
                 mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
-                mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
-                        mNotificationHeaderLowPriority, mContainingNotification);
+                mNotificationHeaderWrapperLowPriority =
+                        (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+                                getContext(),
+                                mNotificationHeaderLowPriority,
+                                mContainingNotification);
                 addView(mNotificationHeaderLowPriority, 0);
                 invalidate();
             } else {
@@ -461,7 +488,9 @@
         return mAttachedChildren;
     }
 
-    /** To be called any time the rows have been updated */
+    /**
+     * To be called any time the rows have been updated
+     */
     public void updateExpansionStates() {
         if (mChildrenExpanded || mUserLocked) {
             // we don't modify it the group is expanded or if we are expanding it
@@ -475,7 +504,6 @@
     }
 
     /**
-     *
      * @return the intrinsic size of this children container, i.e the natural fully expanded state
      */
     public int getIntrinsicHeight() {
@@ -485,7 +513,7 @@
 
     /**
      * @return the intrinsic height with a number of children given
-     *         in @param maxAllowedVisibleChildren
+     * in @param maxAllowedVisibleChildren
      */
     private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
         if (showingAsLowPriority()) {
@@ -539,7 +567,8 @@
 
     /**
      * Update the state of all its children based on a linear layout algorithm.
-     * @param parentState the state of the parent
+     *
+     * @param parentState  the state of the parent
      * @param ambientState the ambient state containing ambient information
      */
     public void updateState(ExpandableViewState parentState, AmbientState ambientState) {
@@ -655,14 +684,17 @@
      * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
      * height, children in the group after this are gone.
      *
-     * @param child the child who's height to adjust.
+     * @param child        the child who's height to adjust.
      * @param parentHeight the height of the parent.
-     * @param childState the state to update.
-     * @param yPosition the yPosition of the view.
+     * @param childState   the state to update.
+     * @param yPosition    the yPosition of the view.
      * @return true if children after this one should be hidden.
      */
-    private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
-            int parentHeight, ExpandableViewState childState, int yPosition) {
+    private boolean updateChildStateForExpandedGroup(
+            ExpandableNotificationRow child,
+            int parentHeight,
+            ExpandableViewState childState,
+            int yPosition) {
         final int top = yPosition + child.getClipTopAmount();
         final int intrinsicHeight = child.getIntrinsicHeight();
         final int bottom = top + intrinsicHeight;
@@ -690,13 +722,15 @@
         if (mIsLowPriority
                 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
                 || (mContainingNotification.isHeadsUpState()
-                        && mContainingNotification.canShowHeadsUp())) {
+                && mContainingNotification.canShowHeadsUp())) {
             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
         }
         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
     }
 
-    /** Applies state to children. */
+    /**
+     * Applies state to children.
+     */
     public void applyState() {
         int childCount = mAttachedChildren.size();
         ViewState tmpState = new ViewState();
@@ -768,17 +802,73 @@
         }
     }
 
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        boolean isCanvasChanged = false;
+
+        Path clipPath = mChildClipPath;
+        if (clipPath != null) {
+            final float translation;
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child;
+                translation = notificationRow.getTranslation();
+            } else {
+                translation = child.getTranslationX();
+            }
+
+            isCanvasChanged = true;
+            canvas.save();
+            if (mIsNotificationGroupCornerEnabled && translation != 0f) {
+                clipPath.offset(translation, 0f);
+                canvas.clipPath(clipPath);
+                clipPath.offset(-translation, 0f);
+            } else {
+                canvas.clipPath(clipPath);
+            }
+        }
+
+        if (child instanceof NotificationHeaderView
+                && mNotificationHeaderWrapper.hasRoundedCorner()) {
+            float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
+            mHeaderPath.reset();
+            mHeaderPath.addRoundRect(
+                    child.getLeft(),
+                    child.getTop(),
+                    child.getRight(),
+                    child.getBottom(),
+                    radii,
+                    Direction.CW
+            );
+            if (!isCanvasChanged) {
+                isCanvasChanged = true;
+                canvas.save();
+            }
+            canvas.clipPath(mHeaderPath);
+        }
+
+        if (isCanvasChanged) {
+            boolean result = super.drawChild(canvas, child, drawingTime);
+            canvas.restore();
+            return result;
+        } else {
+            // If there have been no changes to the canvas we can proceed as usual
+            return super.drawChild(canvas, child, drawingTime);
+        }
+    }
+
+
     /**
      * This is called when the children expansion has changed and positions the children properly
      * for an appear animation.
-     *
      */
     public void prepareExpansionChanged() {
         // TODO: do something that makes sense, like placing the invisible views correctly
         return;
     }
 
-    /** Animate to a given state. */
+    /**
+     * Animate to a given state.
+     */
     public void startAnimationToState(AnimationProperties properties) {
         int childCount = mAttachedChildren.size();
         ViewState tmpState = new ViewState();
@@ -1102,7 +1192,8 @@
      * Get the minimum Height for this group.
      *
      * @param maxAllowedVisibleChildren the number of children that should be visible
-     * @param likeHighPriority if the height should be calculated as if it were not low priority
+     * @param likeHighPriority          if the height should be calculated as if it were not low
+     *                                  priority
      */
     private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
         return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
@@ -1112,10 +1203,13 @@
      * Get the minimum Height for this group.
      *
      * @param maxAllowedVisibleChildren the number of children that should be visible
-     * @param likeHighPriority if the height should be calculated as if it were not low priority
-     * @param headerTranslation the translation amount of the header
+     * @param likeHighPriority          if the height should be calculated as if it were not low
+     *                                  priority
+     * @param headerTranslation         the translation amount of the header
      */
-    private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority,
+    private int getMinHeight(
+            int maxAllowedVisibleChildren,
+            boolean likeHighPriority,
             int headerTranslation) {
         if (!likeHighPriority && showingAsLowPriority()) {
             if (mNotificationHeaderLowPriority == null) {
@@ -1274,16 +1368,19 @@
         return mUserLocked;
     }
 
-    public void setCurrentBottomRoundness(float currentBottomRoundness) {
+    @Override
+    public void applyRoundness() {
+        Roundable.super.applyRoundness();
         boolean last = true;
         for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
             ExpandableNotificationRow child = mAttachedChildren.get(i);
             if (child.getVisibility() == View.GONE) {
                 continue;
             }
-            float bottomRoundness = last ? currentBottomRoundness : 0.0f;
-            child.setBottomRoundness(bottomRoundness, isShown() /* animate */);
-            child.setTopRoundness(0.0f, false /* animate */);
+            child.requestBottomRoundness(
+                    last ? getBottomRoundness() : 0f,
+                    /* animate = */ isShown(),
+                    SourceType.DefaultValue);
             last = false;
         }
     }
@@ -1293,7 +1390,9 @@
         mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mNotificationHeaderWrapper != null) {
             mNotificationHeaderWrapper.setFeedbackIcon(icon);
@@ -1325,4 +1424,26 @@
             child.setNotificationFaded(faded);
         }
     }
+
+    /**
+     * Allow to define a path the clip the children in #drawChild()
+     *
+     * @param childClipPath path used to clip the children
+     */
+    public void setChildClipPath(@Nullable Path childClipPath) {
+        mChildClipPath = childClipPath;
+        invalidate();
+    }
+
+    public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
+        return mNotificationHeaderWrapper;
+    }
+
+    /**
+     * Enable the support for rounded corner in notification group
+     * @param enabled true if is supported
+     */
+    public void enableNotificationGroupCorner(boolean enabled) {
+        mIsNotificationGroupCornerEnabled = enabled;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 2015c87..6810055 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -26,6 +26,8 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -59,8 +61,8 @@
     private boolean mIsClearAllInProgress;
 
     private ExpandableView mSwipedView = null;
-    private ExpandableView mViewBeforeSwipedView = null;
-    private ExpandableView mViewAfterSwipedView = null;
+    private Roundable mViewBeforeSwipedView = null;
+    private Roundable mViewAfterSwipedView = null;
 
     @Inject
     NotificationRoundnessManager(
@@ -101,11 +103,12 @@
     public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
         return expandableView != null
                 && (expandableView == mSwipedView
-                    || expandableView == mViewBeforeSwipedView
-                    || expandableView == mViewAfterSwipedView);
+                || expandableView == mViewBeforeSwipedView
+                || expandableView == mViewAfterSwipedView);
     }
 
-    boolean updateViewWithoutCallback(ExpandableView view,
+    boolean updateViewWithoutCallback(
+            ExpandableView view,
             boolean animate) {
         if (view == null
                 || view == mViewBeforeSwipedView
@@ -113,11 +116,15 @@
             return false;
         }
 
-        final float topRoundness = getRoundnessFraction(view, true /* top */);
-        final float bottomRoundness = getRoundnessFraction(view, false /* top */);
+        final boolean isTopChanged = view.requestTopRoundness(
+                getRoundnessDefaultValue(view, true /* top */),
+                animate,
+                SourceType.DefaultValue);
 
-        final boolean topChanged = view.setTopRoundness(topRoundness, animate);
-        final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate);
+        final boolean isBottomChanged = view.requestBottomRoundness(
+                getRoundnessDefaultValue(view, /* top = */ false),
+                animate,
+                SourceType.DefaultValue);
 
         final boolean isFirstInSection = isFirstInSection(view);
         final boolean isLastInSection = isLastInSection(view);
@@ -126,9 +133,9 @@
         view.setLastInSection(isLastInSection);
 
         mNotifLogger.onCornersUpdated(view, isFirstInSection,
-                isLastInSection, topChanged, bottomChanged);
+                isLastInSection, isTopChanged, isBottomChanged);
 
-        return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged);
+        return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged);
     }
 
     private boolean isFirstInSection(ExpandableView view) {
@@ -150,42 +157,46 @@
     }
 
     void setViewsAffectedBySwipe(
-            ExpandableView viewBefore,
+            Roundable viewBefore,
             ExpandableView viewSwiped,
-            ExpandableView viewAfter) {
+            Roundable viewAfter) {
         final boolean animate = true;
+        final SourceType source = SourceType.OnDismissAnimation;
 
-        ExpandableView oldViewBefore = mViewBeforeSwipedView;
+        // This method requires you to change the roundness of the current View targets and reset
+        // the roundness of the old View targets (if any) to 0f.
+        // To avoid conflicts, it generates a set of old Views and removes the current Views
+        // from this set.
+        HashSet<Roundable> oldViews = new HashSet<>();
+        if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView);
+        if (mSwipedView != null) oldViews.add(mSwipedView);
+        if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
+
         mViewBeforeSwipedView = viewBefore;
-        if (oldViewBefore != null) {
-            final float bottomRoundness = getRoundnessFraction(oldViewBefore, false /* top */);
-            oldViewBefore.setBottomRoundness(bottomRoundness,  animate);
-        }
         if (viewBefore != null) {
-            viewBefore.setBottomRoundness(1f, animate);
+            oldViews.remove(viewBefore);
+            viewBefore.requestTopRoundness(0f, animate, source);
+            viewBefore.requestBottomRoundness(1f, animate, source);
         }
 
-        ExpandableView oldSwipedview = mSwipedView;
         mSwipedView = viewSwiped;
-        if (oldSwipedview != null) {
-            final float bottomRoundness = getRoundnessFraction(oldSwipedview, false /* top */);
-            final float topRoundness = getRoundnessFraction(oldSwipedview, true /* top */);
-            oldSwipedview.setTopRoundness(topRoundness, animate);
-            oldSwipedview.setBottomRoundness(bottomRoundness, animate);
-        }
         if (viewSwiped != null) {
-            viewSwiped.setTopRoundness(1f, animate);
-            viewSwiped.setBottomRoundness(1f, animate);
+            oldViews.remove(viewSwiped);
+            viewSwiped.requestTopRoundness(1f, animate, source);
+            viewSwiped.requestBottomRoundness(1f, animate, source);
         }
 
-        ExpandableView oldViewAfter = mViewAfterSwipedView;
         mViewAfterSwipedView = viewAfter;
-        if (oldViewAfter != null) {
-            final float topRoundness = getRoundnessFraction(oldViewAfter, true /* top */);
-            oldViewAfter.setTopRoundness(topRoundness, animate);
-        }
         if (viewAfter != null) {
-            viewAfter.setTopRoundness(1f, animate);
+            oldViews.remove(viewAfter);
+            viewAfter.requestTopRoundness(1f, animate, source);
+            viewAfter.requestBottomRoundness(0f, animate, source);
+        }
+
+        // After setting the current Views, reset the views that are still present in the set.
+        for (Roundable oldView : oldViews) {
+            oldView.requestTopRoundness(0f, animate, source);
+            oldView.requestBottomRoundness(0f, animate, source);
         }
     }
 
@@ -193,7 +204,7 @@
         mIsClearAllInProgress = isClearingAll;
     }
 
-    private float getRoundnessFraction(ExpandableView view, boolean top) {
+    private float getRoundnessDefaultValue(Roundable view, boolean top) {
         if (view == null) {
             return 0f;
         }
@@ -207,28 +218,35 @@
                 && mIsClearAllInProgress) {
             return 1.0f;
         }
-        if ((view.isPinned()
-                || (view.isHeadsUpAnimatingAway()) && !mExpanded)) {
-            return 1.0f;
-        }
-        if (isFirstInSection(view) && top) {
-            return 1.0f;
-        }
-        if (isLastInSection(view) && !top) {
-            return 1.0f;
-        }
+        if (view instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) view;
+            if ((expandableView.isPinned()
+                    || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) {
+                return 1.0f;
+            }
+            if (isFirstInSection(expandableView) && top) {
+                return 1.0f;
+            }
+            if (isLastInSection(expandableView) && !top) {
+                return 1.0f;
+            }
 
-        if (view == mTrackedHeadsUp) {
-            // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
-            // rounded.
-            return MathUtils.saturate(1.0f - mAppearFraction);
+            if (view == mTrackedHeadsUp) {
+                // If we're pushing up on a headsup the appear fraction is < 0 and it needs to
+                // still be rounded.
+                return MathUtils.saturate(1.0f - mAppearFraction);
+            }
+            if (expandableView.showingPulsing() && mRoundForPulsingViews) {
+                return 1.0f;
+            }
+            if (expandableView.isChildInGroup()) {
+                return 0f;
+            }
+            final Resources resources = expandableView.getResources();
+            return resources.getDimension(R.dimen.notification_corner_radius_small)
+                    / resources.getDimension(R.dimen.notification_corner_radius);
         }
-        if (view.showingPulsing() && mRoundForPulsingViews) {
-            return 1.0f;
-        }
-        final Resources resources = view.getResources();
-        return resources.getDimension(R.dimen.notification_corner_radius_small)
-                / resources.getDimension(R.dimen.notification_corner_radius);
+        return 0f;
     }
 
     public void setExpanded(float expandedHeight, float appearFraction) {
@@ -258,8 +276,10 @@
         mNotifLogger.onSectionCornersUpdated(sections, anyChanged);
     }
 
-    private boolean handleRemovedOldViews(NotificationSection[] sections,
-            ExpandableView[] oldViews, boolean first) {
+    private boolean handleRemovedOldViews(
+            NotificationSection[] sections,
+            ExpandableView[] oldViews,
+            boolean first) {
         boolean anyChanged = false;
         for (ExpandableView oldView : oldViews) {
             if (oldView != null) {
@@ -289,8 +309,10 @@
         return anyChanged;
     }
 
-    private boolean handleAddedNewViews(NotificationSection[] sections,
-            ExpandableView[] oldViews, boolean first) {
+    private boolean handleAddedNewViews(
+            NotificationSection[] sections,
+            ExpandableView[] oldViews,
+            boolean first) {
         boolean anyChanged = false;
         for (NotificationSection section : sections) {
             ExpandableView newView =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 91a2813..a1b77ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -19,11 +19,10 @@
 import android.util.Log
 import android.view.View
 import com.android.internal.annotations.VisibleForTesting
-import com.android.systemui.media.KeyguardMediaController
+import com.android.systemui.media.controls.ui.KeyguardMediaController
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
 import com.android.systemui.statusbar.notification.collection.render.MediaContainerController
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
-import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager
 import com.android.systemui.statusbar.notification.dagger.AlertingHeader
 import com.android.systemui.statusbar.notification.dagger.IncomingHeader
 import com.android.systemui.statusbar.notification.dagger.PeopleHeader
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 2272411..df705c5 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
@@ -1188,7 +1188,7 @@
             return;
         }
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (mChildrenToAddAnimated.contains(child)) {
                 final int startingPosition = getPositionInLinearLayout(child);
                 final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements;
@@ -1658,7 +1658,7 @@
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+            ExpandableView slidingChild = getChildAtIndex(childIdx);
             if (slidingChild.getVisibility() != VISIBLE
                     || (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) {
                 continue;
@@ -1691,6 +1691,10 @@
         return null;
     }
 
+    private ExpandableView getChildAtIndex(int index) {
+        return (ExpandableView) getChildAt(index);
+    }
+
     public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
         getLocationOnScreen(mTempInt2);
         return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
@@ -2276,7 +2280,7 @@
         int childCount = getChildCount();
         int count = 0;
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
                 count++;
             }
@@ -2496,7 +2500,7 @@
     private ExpandableView getLastChildWithBackground() {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
                 return child;
@@ -2509,7 +2513,7 @@
     private ExpandableView getFirstChildWithBackground() {
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
                 return child;
@@ -2523,7 +2527,7 @@
         ArrayList<ExpandableView> children = new ArrayList<>();
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE
                     && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
@@ -2882,7 +2886,7 @@
         }
         int position = 0;
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             boolean notGone = child.getVisibility() != View.GONE;
             if (notGone && !child.hasNoContentHeight()) {
                 if (position != 0) {
@@ -2936,7 +2940,7 @@
         }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
         // TODO: Refactor SectionManager and put the RoundnessManager there.
-        mController.getNoticationRoundessManager().updateRoundedChildren(mSections);
+        mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
         mAnimateBottomOnLayout = false;
         invalidate();
     }
@@ -3968,7 +3972,7 @@
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void clearUserLockedViews() {
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                 row.setUserLocked(false);
@@ -3981,7 +3985,7 @@
         // lets make sure nothing is transient anymore
         clearTemporaryViewsInGroup(this);
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                 clearTemporaryViewsInGroup(row.getChildrenContainer());
@@ -4230,7 +4234,7 @@
         if (hideSensitive != mAmbientState.isHideSensitive()) {
             int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
-                ExpandableView v = (ExpandableView) getChildAt(i);
+                ExpandableView v = getChildAtIndex(i);
                 v.setHideSensitiveForIntrinsicHeight(hideSensitive);
             }
             mAmbientState.setHideSensitive(hideSensitive);
@@ -4265,7 +4269,7 @@
     private void applyCurrentState() {
         int numChildren = getChildCount();
         for (int i = 0; i < numChildren; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             child.applyViewState();
         }
 
@@ -4285,7 +4289,7 @@
 
         // Lefts first sort by Z difference
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != GONE) {
                 mTmpSortedChildren.add(child);
             }
@@ -4512,7 +4516,7 @@
     public void setClearAllInProgress(boolean clearAllInProgress) {
         mClearAllInProgress = clearAllInProgress;
         mAmbientState.setClearAllInProgress(clearAllInProgress);
-        mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress);
+        mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
     }
 
     boolean getClearAllInProgress() {
@@ -4555,7 +4559,7 @@
         final int count = getChildCount();
         float max = 0;
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            ExpandableView child = (ExpandableView) getChildAt(childIdx);
+            ExpandableView child = getChildAtIndex(childIdx);
             if (child.getVisibility() == GONE) {
                 continue;
             }
@@ -4586,7 +4590,7 @@
     public boolean isBelowLastNotification(float touchX, float touchY) {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE) {
                 float childTop = child.getY();
                 if (childTop > touchY) {
@@ -5052,7 +5056,7 @@
             pw.println();
 
             for (int i = 0; i < childCount; i++) {
-                ExpandableView child = (ExpandableView) getChildAt(i);
+                ExpandableView child = getChildAtIndex(i);
                 child.dump(pw, args);
                 pw.println();
             }
@@ -5341,7 +5345,7 @@
         float wakeUplocation = -1f;
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView view = (ExpandableView) getChildAt(i);
+            ExpandableView view = getChildAtIndex(i);
             if (view.getVisibility() == View.GONE) {
                 continue;
             }
@@ -5380,7 +5384,7 @@
     public void setController(
             NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
         mController = notificationStackScrollLayoutController;
-        mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated);
+        mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
     }
 
     void addSwipedOutView(View v) {
@@ -5391,31 +5395,22 @@
         if (!(viewSwiped instanceof ExpandableNotificationRow)) {
             return;
         }
-        final int indexOfSwipedView = indexOfChild(viewSwiped);
-        if (indexOfSwipedView < 0) {
-            return;
-        }
         mSectionsManager.updateFirstAndLastViewsForAllSections(
-                mSections, getChildrenWithBackground());
-        View viewBefore = null;
-        if (indexOfSwipedView > 0) {
-            viewBefore = getChildAt(indexOfSwipedView - 1);
-            if (mSectionsManager.beginsSection(viewSwiped, viewBefore)) {
-                viewBefore = null;
-            }
-        }
-        View viewAfter = null;
-        if (indexOfSwipedView < getChildCount()) {
-            viewAfter = getChildAt(indexOfSwipedView + 1);
-            if (mSectionsManager.beginsSection(viewAfter, viewSwiped)) {
-                viewAfter = null;
-            }
-        }
-        mController.getNoticationRoundessManager()
+                mSections,
+                getChildrenWithBackground()
+        );
+
+        RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets(
+                (ExpandableNotificationRow) viewSwiped,
+                this,
+                mSectionsManager
+        );
+
+        mController.getNotificationRoundnessManager()
                 .setViewsAffectedBySwipe(
-                        (ExpandableView) viewBefore,
-                        (ExpandableView) viewSwiped,
-                        (ExpandableView) viewAfter);
+                        targets.getBefore(),
+                        targets.getSwiped(),
+                        targets.getAfter());
 
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
@@ -5426,7 +5421,7 @@
 
     void onSwipeEnd() {
         updateFirstAndLastBackgroundViews();
-        mController.getNoticationRoundessManager()
+        mController.getNotificationRoundnessManager()
                 .setViewsAffectedBySwipe(null, null, null);
         // Round bottom corners for notification right before shelf.
         mShelf.updateAppearance();
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 5c09d61..e1337826 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
@@ -63,7 +63,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
@@ -180,6 +180,7 @@
     private int mBarState;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
     private final FeatureFlags mFeatureFlags;
+    private final NotificationTargetsHelper mNotificationTargetsHelper;
 
     private View mLongPressedView;
 
@@ -642,7 +643,8 @@
             StackStateLogger stackLogger,
             NotificationStackScrollLogger logger,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            NotificationTargetsHelper notificationTargetsHelper) {
         mStackStateLogger = stackLogger;
         mLogger = logger;
         mAllowLongPress = allowLongPress;
@@ -679,6 +681,7 @@
         mRemoteInputManager = remoteInputManager;
         mShadeController = shadeController;
         mFeatureFlags = featureFlags;
+        mNotificationTargetsHelper = notificationTargetsHelper;
         updateResources();
     }
 
@@ -1380,7 +1383,7 @@
         return mView.calculateGapHeight(previousView, child, count);
     }
 
-    NotificationRoundnessManager getNoticationRoundessManager() {
+    NotificationRoundnessManager getNotificationRoundnessManager() {
         return mNotificationRoundnessManager;
     }
 
@@ -1537,6 +1540,10 @@
         mNotificationActivityStarter = activityStarter;
     }
 
+    public NotificationTargetsHelper getNotificationTargetsHelper() {
+        return mNotificationTargetsHelper;
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
new file mode 100644
index 0000000..991a14b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.statusbar.notification.stack
+
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.Roundable
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import javax.inject.Inject
+
+/**
+ * Utility class that helps us find the targets of an animation, often used to find the notification
+ * ([Roundable]) above and below the current one (see [findRoundableTargets]).
+ */
+@SysUISingleton
+class NotificationTargetsHelper
+@Inject
+constructor(
+    featureFlags: FeatureFlags,
+) {
+    private val isNotificationGroupCornerEnabled =
+        featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)
+
+    /**
+     * This method looks for views that can be rounded (and implement [Roundable]) during a
+     * notification swipe.
+     * @return The [Roundable] targets above/below the [viewSwiped] (if available). The
+     * [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is
+     * no above/below notification or the notification is not part of the same section.
+     */
+    fun findRoundableTargets(
+        viewSwiped: ExpandableNotificationRow,
+        stackScrollLayout: NotificationStackScrollLayout,
+        sectionsManager: NotificationSectionsManager,
+    ): RoundableTargets {
+        val viewBefore: Roundable?
+        val viewAfter: Roundable?
+
+        val notificationParent = viewSwiped.notificationParent
+        val childrenContainer = notificationParent?.childrenContainer
+        val visibleStackChildren =
+            stackScrollLayout.children
+                .filterIsInstance<ExpandableView>()
+                .filter { it.isVisible }
+                .toList()
+        if (notificationParent != null && childrenContainer != null) {
+            // We are inside a notification group
+
+            if (!isNotificationGroupCornerEnabled) {
+                return RoundableTargets(null, null, null)
+            }
+
+            val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible }
+            val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped)
+
+            viewBefore =
+                visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1)
+                    ?: childrenContainer.notificationHeaderWrapper
+
+            viewAfter =
+                visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1)
+                    ?: visibleStackChildren.indexOf(notificationParent).let {
+                        visibleStackChildren.getOrNull(it + 1)
+                    }
+        } else {
+            // Assumption: we are inside the NotificationStackScrollLayout
+
+            val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped)
+
+            viewBefore =
+                visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf {
+                    !sectionsManager.beginsSection(viewSwiped, it)
+                }
+
+            viewAfter =
+                visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf {
+                    !sectionsManager.beginsSection(it, viewSwiped)
+                }
+        }
+
+        return RoundableTargets(
+            before = viewBefore,
+            swiped = viewSwiped,
+            after = viewAfter,
+        )
+    }
+}
+
+/**
+ * This object contains targets above/below the [swiped] (if available). The [before] and [after]
+ * parameters can be `null` if there is no above/below notification or the notification is not part
+ * of the same section.
+ */
+data class RoundableTargets(
+    val before: Roundable?,
+    val swiped: ExpandableNotificationRow?,
+    val after: Roundable?,
+)
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 0502159..eea1d911 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
@@ -31,6 +31,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -804,7 +805,7 @@
                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
         final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
                 ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
-        row.setBottomRoundness(roundness, /* animate= */ false);
+        row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll);
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 25fd483..70cf56d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -262,8 +262,6 @@
     @Override
     void startActivity(Intent intent, boolean dismissShade, Callback callback);
 
-    void setQsExpanded(boolean expanded);
-
     boolean isWakeUpComingFromTouch();
 
     boolean isFalsingThresholdNeeded();
@@ -455,6 +453,9 @@
 
     void collapseShade();
 
+    /** Collapse the shade, but conditional on a flag specific to the trigger of a bugreport. */
+    void collapseShadeForBugreport();
+
     int getWakefulnessState();
 
     boolean isScreenFullyOff();
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 2c834cf..29642be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -868,6 +868,11 @@
             mBubblesOptional.get().setExpandListener(mBubbleExpandListener);
         }
 
+        // Do not restart System UI when the bugreport flag changes.
+        mFeatureFlags.addListener(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, event -> {
+            event.requestNoRestart();
+        });
+
         mStatusBarSignalPolicy.init();
         mKeyguardIndicationController.init();
 
@@ -1772,18 +1777,6 @@
     }
 
     @Override
-    public void setQsExpanded(boolean expanded) {
-        mNotificationShadeWindowController.setQsExpanded(expanded);
-        mNotificationPanelViewController.setStatusAccessibilityImportance(expanded
-                ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        mNotificationPanelViewController.updateSystemUiStateFlags();
-        if (getNavigationBarView() != null) {
-            getNavigationBarView().onStatusBarPanelStateChanged();
-        }
-    }
-
-    @Override
     public boolean isWakeUpComingFromTouch() {
         return mWakeUpComingFromTouch;
     }
@@ -3561,6 +3554,13 @@
         }
     }
 
+    @Override
+    public void collapseShadeForBugreport() {
+        if (!mFeatureFlags.isEnabled(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT)) {
+            collapseShade();
+        }
+    }
+
     @VisibleForTesting
     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
index b987f68..b965ac9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm
@@ -95,14 +96,7 @@
     var bouncerShowing: Boolean = false
     var altBouncerShowing: Boolean = false
     var launchingAffordance: Boolean = false
-    var qSExpanded = false
-        set(value) {
-            val changed = field != value
-            field = value
-            if (changed && !value) {
-                maybePerformPendingUnlock()
-            }
-        }
+    var qsExpanded = false
 
     @Inject
     constructor(
@@ -111,6 +105,7 @@
         statusBarStateController: StatusBarStateController,
         lockscreenUserManager: NotificationLockscreenUserManager,
         keyguardStateController: KeyguardStateController,
+        shadeExpansionStateManager: ShadeExpansionStateManager,
         dumpManager: DumpManager
     ) {
         this.mKeyguardStateController = keyguardStateController
@@ -132,6 +127,14 @@
             }
         })
 
+        shadeExpansionStateManager.addQsExpansionListener { isQsExpanded ->
+            val changed = qsExpanded != isQsExpanded
+            qsExpanded = isQsExpanded
+            if (changed && !isQsExpanded) {
+                maybePerformPendingUnlock()
+            }
+        }
+
         val dismissByDefault = if (context.resources.getBoolean(
                         com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0
         tunerService.addTunable(object : TunerService.Tunable {
@@ -160,7 +163,7 @@
     ): Boolean {
         if (biometricSourceType == BiometricSourceType.FACE && bypassEnabled) {
             val can = canBypass()
-            if (!can && (isPulseExpanding || qSExpanded)) {
+            if (!can && (isPulseExpanding || qsExpanded)) {
                 pendingUnlock = PendingUnlock(biometricSourceType, isStrongBiometric)
             }
             return can
@@ -189,7 +192,7 @@
                 altBouncerShowing -> true
                 statusBarStateController.state != StatusBarState.KEYGUARD -> false
                 launchingAffordance -> false
-                isPulseExpanding || qSExpanded -> false
+                isPulseExpanding || qsExpanded -> false
                 else -> true
             }
         }
@@ -214,7 +217,7 @@
         pw.println("  altBouncerShowing: $altBouncerShowing")
         pw.println("  isPulseExpanding: $isPulseExpanding")
         pw.println("  launchingAffordance: $launchingAffordance")
-        pw.println("  qSExpanded: $qSExpanded")
+        pw.println("  qSExpanded: $qsExpanded")
         pw.println("  hasFaceFeature: $hasFaceFeature")
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index 00c3e8f..5e2a7c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
@@ -26,6 +26,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
@@ -67,7 +68,7 @@
                         ActivityLaunchAnimator.Controller.fromView(v, null),
                         true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM);
             } else {
-                mUserSwitchDialogController.showDialog(v);
+                mUserSwitchDialogController.showDialog(v.getContext(), Expandable.fromView(v));
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index ece7ee0..86f6ff8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -372,7 +372,7 @@
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
 
-            if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (statusBarPipelineFlags.useNewMobileIcons()) {
                 // This starts the flow for the new pipeline, and will notify us of changes
                 mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
                 MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
@@ -451,7 +451,7 @@
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
             final BaseStatusBarFrameLayout view;
-            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (mStatusBarPipelineFlags.useNewWifiIcon()) {
                 view = onCreateModernStatusBarWifiView(slot);
                 // When [ModernStatusBarWifiView] is created, it will automatically apply the
                 // correct view state so we don't need to call applyWifiState.
@@ -474,9 +474,9 @@
                 String slot,
                 MobileIconState state
         ) {
-            if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (mStatusBarPipelineFlags.useNewMobileIcons()) {
                 throw new IllegalStateException("Attempting to add a mobile icon while the new "
-                        + "pipeline is enabled is not supported");
+                        + "icons are enabled is not supported");
             }
 
             // Use the `subId` field as a key to query for the correct context
@@ -497,7 +497,7 @@
                 String slot,
                 int subId
         ) {
-            if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+            if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
                 throw new IllegalStateException("Attempting to add a mobile icon using the new"
                         + "pipeline, but the enabled flag is false.");
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index e106b9e..31e960a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -224,9 +224,9 @@
      */
     @Override
     public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
-        if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+        if (mStatusBarPipelineFlags.useNewMobileIcons()) {
             Log.d(TAG, "ignoring old pipeline callbacks, because the new "
-                    + "pipeline frontend is enabled");
+                    + "icons are enabled");
             return;
         }
         Slot mobileSlot = mStatusBarIconList.getSlot(slot);
@@ -249,9 +249,9 @@
 
     @Override
     public void setNewMobileIconSubIds(List<Integer> subIds) {
-        if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) {
+        if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
             Log.d(TAG, "ignoring new pipeline callback, "
-                    + "since the frontend is disabled");
+                    + "since the new icons are disabled");
             return;
         }
         Slot mobileSlot = mStatusBarIconList.getSlot("mobile");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 5f5ec68..5480f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -60,7 +60,6 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.data.BouncerView;
-import com.android.systemui.keyguard.data.BouncerViewDelegate;
 import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
 import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
 import com.android.systemui.navigationbar.NavigationBarView;
@@ -136,7 +135,7 @@
     private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController;
     private final BouncerCallbackInteractor mBouncerCallbackInteractor;
     private final BouncerInteractor mBouncerInteractor;
-    private final BouncerViewDelegate mBouncerViewDelegate;
+    private final BouncerView mBouncerView;
     private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
 
     private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() {
@@ -327,7 +326,7 @@
         mKeyguardSecurityModel = keyguardSecurityModel;
         mBouncerCallbackInteractor = bouncerCallbackInteractor;
         mBouncerInteractor = bouncerInteractor;
-        mBouncerViewDelegate = bouncerView.getDelegate();
+        mBouncerView = bouncerView;
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
         mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
@@ -804,7 +803,7 @@
     private void setDozing(boolean dozing) {
         if (mDozing != dozing) {
             mDozing = dozing;
-            if (dozing || mBouncer.needsFullscreenBouncer()
+            if (dozing || needsFullscreenBouncer()
                     || mKeyguardStateController.isOccluded()) {
                 reset(dozing /* hideBouncerWhenShowing */);
             }
@@ -1081,7 +1080,7 @@
      * @return whether a back press can be handled right now.
      */
     public boolean canHandleBackPressed() {
-        return mBouncer.isShowing();
+        return bouncerIsShowing();
     }
 
     /**
@@ -1094,7 +1093,7 @@
 
         mCentralSurfaces.endAffordanceLaunch();
         // The second condition is for SIM card locked bouncer
-        if (bouncerIsScrimmed() && needsFullscreenBouncer()) {
+        if (bouncerIsScrimmed() && !needsFullscreenBouncer()) {
             hideBouncer(false);
             updateStates();
         } else {
@@ -1124,8 +1123,8 @@
     }
 
     public boolean isFullscreenBouncer() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.isFullScreenBouncer();
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().isFullScreenBouncer();
         }
         return mBouncer != null && mBouncer.isFullscreenBouncer();
     }
@@ -1284,15 +1283,15 @@
     }
 
     public boolean shouldDismissOnMenuPressed() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.shouldDismissOnMenuPressed();
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().shouldDismissOnMenuPressed();
         }
         return mBouncer != null && mBouncer.shouldDismissOnMenuPressed();
     }
 
     public boolean interceptMediaKey(KeyEvent event) {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.interceptMediaKey(event);
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().interceptMediaKey(event);
         }
         return mBouncer != null && mBouncer.interceptMediaKey(event);
     }
@@ -1301,8 +1300,8 @@
      * @return true if the pre IME back event should be handled
      */
     public boolean dispatchBackKeyEventPreIme() {
-        if (mBouncerViewDelegate != null) {
-            return mBouncerViewDelegate.dispatchBackKeyEventPreIme();
+        if (mBouncerView.getDelegate() != null) {
+            return mBouncerView.getDelegate().dispatchBackKeyEventPreIme();
         }
         return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
index 0d52f46..e498ae4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherController.kt
@@ -19,6 +19,7 @@
 import android.content.Intent
 import android.os.UserHandle
 import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.ActivityStarter
@@ -75,7 +76,7 @@
                         null /* ActivityLaunchAnimator.Controller */,
                         true /* showOverlockscreenwhenlocked */, UserHandle.SYSTEM)
             } else {
-                userSwitcherDialogController.showDialog(view)
+                userSwitcherDialogController.showDialog(view.context, Expandable.fromView(view))
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 9b8b643..06cd12d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -24,29 +24,19 @@
 /** All flagging methods related to the new status bar pipeline (see b/238425913). */
 @SysUISingleton
 class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
-    /**
-     * Returns true if we should run the new pipeline backend.
-     *
-     * The new pipeline backend hooks up to all our external callbacks, logs those callback inputs,
-     * and logs the output state.
-     */
-    fun isNewPipelineBackendEnabled(): Boolean =
-        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_BACKEND)
+    /** True if we should display the mobile icons using the new status bar data pipeline. */
+    fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS)
+
+    /** True if we should display the wifi icon using the new status bar data pipeline. */
+    fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON)
+
+    // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the
+    //   logging without getting the UI effects.
 
     /**
-     * Returns true if we should run the new pipeline frontend *and* backend.
-     *
-     * The new pipeline frontend will use the outputted state from the new backend and will make the
-     * correct changes to the UI.
-     */
-    fun isNewPipelineFrontendEnabled(): Boolean =
-        isNewPipelineBackendEnabled() &&
-            featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE_FRONTEND)
-
-    /**
-     * Returns true if we should apply some coloring to icons that were rendered with the new
+     * Returns true if we should apply some coloring to the wifi icon that was rendered with the new
      * pipeline to help with debugging.
      */
-    // For now, just always apply the debug coloring if we've enabled frontend rendering.
-    fun useNewPipelineDebugColoring(): Boolean = isNewPipelineFrontendEnabled()
+    // For now, just always apply the debug coloring if we've enabled the new icon.
+    fun useWifiDebugColoring(): Boolean = useNewWifiIcon()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
new file mode 100644
index 0000000..7aa5ee1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.data.repository
+
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings.Global
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.SettingObserver
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
+import com.android.systemui.util.settings.GlobalSettings
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides data related to airplane mode.
+ *
+ * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. It is
+ * only used to help [com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel]
+ * determine what parts of the wifi icon view should be shown.
+ *
+ * TODO(b/238425913): Consider migrating the status bar airplane mode icon to use this repo.
+ */
+interface AirplaneModeRepository {
+    /** Observable for whether the device is currently in airplane mode. */
+    val isAirplaneMode: StateFlow<Boolean>
+}
+
+@SysUISingleton
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class AirplaneModeRepositoryImpl
+@Inject
+constructor(
+    @Background private val bgHandler: Handler,
+    private val globalSettings: GlobalSettings,
+    logger: ConnectivityPipelineLogger,
+    @Application scope: CoroutineScope,
+) : AirplaneModeRepository {
+    // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it.
+    override val isAirplaneMode: StateFlow<Boolean> =
+        conflatedCallbackFlow {
+                val observer =
+                    object :
+                        SettingObserver(
+                            globalSettings,
+                            bgHandler,
+                            Global.AIRPLANE_MODE_ON,
+                            UserHandle.USER_ALL
+                        ) {
+                        override fun handleValueChanged(value: Int, observedChange: Boolean) {
+                            trySend(value == 1)
+                        }
+                    }
+
+                observer.isListening = true
+                trySend(observer.value == 1)
+                awaitClose { observer.isListening = false }
+            }
+            .distinctUntilChanged()
+            .logInputChange(logger, "isAirplaneMode")
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                // When the observer starts listening, the flow will emit the current value so the
+                // initialValue here is irrelevant.
+                initialValue = false,
+            )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt
new file mode 100644
index 0000000..3e9b2c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * The business logic layer for airplane mode.
+ *
+ * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See
+ * [AirplaneModeRepository] for more details.
+ */
+@SysUISingleton
+class AirplaneModeInteractor
+@Inject
+constructor(
+    airplaneModeRepository: AirplaneModeRepository,
+    connectivityRepository: ConnectivityRepository,
+) {
+    /** True if the device is currently in airplane mode. */
+    val isAirplaneMode: Flow<Boolean> = airplaneModeRepository.isAirplaneMode
+
+    /** True if we're configured to force-hide the airplane mode icon and false otherwise. */
+    val isForceHidden: Flow<Boolean> =
+        connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.AIRPLANE) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
new file mode 100644
index 0000000..fe30c01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Models the UI state for the status bar airplane mode icon.
+ *
+ * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See
+ * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for
+ * more details.
+ */
+@SysUISingleton
+class AirplaneModeViewModel
+@Inject
+constructor(
+    interactor: AirplaneModeInteractor,
+    logger: ConnectivityPipelineLogger,
+    @Application private val scope: CoroutineScope,
+) {
+    /** True if the airplane mode icon is currently visible in the status bar. */
+    val isAirplaneModeIconVisible: StateFlow<Boolean> =
+        combine(interactor.isAirplaneMode, interactor.isForceHidden) {
+                isAirplaneMode,
+                isAirplaneIconForceHidden ->
+                isAirplaneMode && !isAirplaneIconForceHidden
+            }
+            .distinctUntilChanged()
+            .logOutputChange(logger, "isAirplaneModeIconVisible")
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+}
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 06d5542..fcd1b8a 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
@@ -16,10 +16,16 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -30,16 +36,25 @@
 @Module
 abstract class StatusBarPipelineModule {
     @Binds
+    abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository
+
+    @Binds
     abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
 
     @Binds
     abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
 
     @Binds
-    abstract fun mobileSubscriptionRepository(
-        impl: MobileSubscriptionRepositoryImpl
-    ): MobileSubscriptionRepository
+    abstract fun mobileConnectionsRepository(
+        impl: MobileConnectionsRepositoryImpl
+    ): MobileConnectionsRepository
 
     @Binds
     abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
+
+    @Binds
+    abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
+
+    @Binds
+    abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
index 46ccf32c..eaba0e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
@@ -27,6 +27,7 @@
 import android.telephony.TelephonyCallback.SignalStrengthsListener
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 
 /**
  * Data class containing all of the relevant information for a particular line of service, known as
@@ -57,6 +58,11 @@
     /** From [CarrierNetworkListener.onCarrierNetworkChange] */
     val carrierNetworkChangeActive: Boolean? = null,
 
-    /** From [DisplayInfoListener.onDisplayInfoChanged] */
-    val displayInfo: TelephonyDisplayInfo? = null
+    /**
+     * From [DisplayInfoListener.onDisplayInfoChanged].
+     *
+     * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
+     * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
+     */
+    val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
new file mode 100644
index 0000000..f385806
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import android.telephony.Annotation.NetworkType
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+
+/**
+ * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending
+ * on whether or not the display info contains an override type, we may have to call different
+ * methods on [MobileMappingsProxy] to generate an icon lookup key.
+ */
+sealed interface ResolvedNetworkType {
+    @NetworkType val type: Int
+}
+
+data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
+
+data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
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
new file mode 100644
index 0000000..45284cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
+import android.telephony.TelephonyManager
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.lang.IllegalStateException
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
+ * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this
+ * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId
+ *
+ * There should only ever be one [MobileConnectionRepository] per subscription, since
+ * [TelephonyManager] limits the number of callbacks that can be registered per process.
+ *
+ * This repository should have all of the relevant information for a single line of service, which
+ * eventually becomes a single icon in the status bar.
+ */
+interface MobileConnectionRepository {
+    /**
+     * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
+     * listener + model.
+     */
+    val subscriptionModelFlow: Flow<MobileSubscriptionModel>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileConnectionRepositoryImpl(
+    private val subId: Int,
+    telephonyManager: TelephonyManager,
+    bgDispatcher: CoroutineDispatcher,
+    logger: ConnectivityPipelineLogger,
+    scope: CoroutineScope,
+) : MobileConnectionRepository {
+    init {
+        if (telephonyManager.subscriptionId != subId) {
+            throw IllegalStateException(
+                "TelephonyManager should be created with subId($subId). " +
+                    "Found ${telephonyManager.subscriptionId} instead."
+            )
+        }
+    }
+
+    override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
+        var state = MobileSubscriptionModel()
+        conflatedCallbackFlow {
+                // TODO (b/240569788): log all of these into the connectivity logger
+                val callback =
+                    object :
+                        TelephonyCallback(),
+                        TelephonyCallback.ServiceStateListener,
+                        TelephonyCallback.SignalStrengthsListener,
+                        TelephonyCallback.DataConnectionStateListener,
+                        TelephonyCallback.DataActivityListener,
+                        TelephonyCallback.CarrierNetworkListener,
+                        TelephonyCallback.DisplayInfoListener {
+                        override fun onServiceStateChanged(serviceState: ServiceState) {
+                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            trySend(state)
+                        }
+
+                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+                            val cdmaLevel =
+                                signalStrength
+                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+                                    .let { strengths ->
+                                        if (!strengths.isEmpty()) {
+                                            strengths[0].level
+                                        } else {
+                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                                        }
+                                    }
+
+                            val primaryLevel = signalStrength.level
+
+                            state =
+                                state.copy(
+                                    cdmaLevel = cdmaLevel,
+                                    primaryLevel = primaryLevel,
+                                    isGsm = signalStrength.isGsm,
+                                )
+                            trySend(state)
+                        }
+
+                        override fun onDataConnectionStateChanged(
+                            dataState: Int,
+                            networkType: Int
+                        ) {
+                            state = state.copy(dataConnectionState = dataState)
+                            trySend(state)
+                        }
+
+                        override fun onDataActivity(direction: Int) {
+                            state = state.copy(dataActivityDirection = direction)
+                            trySend(state)
+                        }
+
+                        override fun onCarrierNetworkChange(active: Boolean) {
+                            state = state.copy(carrierNetworkChangeActive = active)
+                            trySend(state)
+                        }
+
+                        override fun onDisplayInfoChanged(
+                            telephonyDisplayInfo: TelephonyDisplayInfo
+                        ) {
+                            val networkType =
+                                if (
+                                    telephonyDisplayInfo.overrideNetworkType ==
+                                        OVERRIDE_NETWORK_TYPE_NONE
+                                ) {
+                                    DefaultNetworkType(telephonyDisplayInfo.networkType)
+                                } else {
+                                    OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
+                                }
+                            state = state.copy(resolvedNetworkType = networkType)
+                            trySend(state)
+                        }
+                    }
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+    }
+
+    class Factory
+    @Inject
+    constructor(
+        private val telephonyManager: TelephonyManager,
+        private val logger: ConnectivityPipelineLogger,
+        @Background private val bgDispatcher: CoroutineDispatcher,
+        @Application private val scope: CoroutineScope,
+    ) {
+        fun build(subId: Int): MobileConnectionRepository {
+            return MobileConnectionRepositoryImpl(
+                subId,
+                telephonyManager.createForSubscriptionId(subId),
+                bgDispatcher,
+                logger,
+                scope,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
new file mode 100644
index 0000000..0e2428a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.content.Context
+import android.content.IntentFilter
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
+ * on various policy
+ */
+interface MobileConnectionsRepository {
+    /** Observable list of current mobile subscriptions */
+    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+
+    /** Observable for the subscriptionId of the current mobile data connection */
+    val activeMobileDataSubscriptionId: Flow<Int>
+
+    /** Observable for [MobileMappings.Config] tracking the defaults */
+    val defaultDataSubRatConfig: StateFlow<Config>
+
+    /** Get or create a repository for the line of service for the given subscription ID */
+    fun getRepoForSubId(subId: Int): MobileConnectionRepository
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileConnectionsRepositoryImpl
+@Inject
+constructor(
+    private val subscriptionManager: SubscriptionManager,
+    private val telephonyManager: TelephonyManager,
+    private val logger: ConnectivityPipelineLogger,
+    broadcastDispatcher: BroadcastDispatcher,
+    private val context: Context,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+) : MobileConnectionsRepository {
+    private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+
+    /**
+     * State flow that emits the set of mobile data subscriptions, each represented by its own
+     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+     * info object, but for now we keep track of the infos themselves.
+     */
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+            }
+            .mapLatest { fetchSubscriptionsList() }
+            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+    /** StateFlow that keeps track of the current active mobile data subscription */
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+                            trySend(subId)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID
+            )
+
+    private val defaultDataSubChangedEvent =
+        broadcastDispatcher.broadcastFlow(
+            IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+        )
+
+    private val carrierConfigChangedEvent =
+        broadcastDispatcher.broadcastFlow(
+            IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+        )
+
+    /**
+     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+     * config, so this will apply to every icon that we care about.
+     *
+     * Relevant bits in the config are things like
+     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+     *
+     * This flow will produce whenever the default data subscription or the carrier config changes.
+     */
+    override val defaultDataSubRatConfig: StateFlow<Config> =
+        combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ ->
+                Config.readConfig(context)
+            }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                initialValue = Config.readConfig(context)
+            )
+
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        if (!isValidSubId(subId)) {
+            throw IllegalArgumentException(
+                "subscriptionId $subId is not in the list of valid subscriptions"
+            )
+        }
+
+        return subIdRepositoryCache[subId]
+            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+    }
+
+    private fun isValidSubId(subId: Int): Boolean {
+        subscriptionsFlow.value.forEach {
+            if (it.subscriptionId == subId) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
+        return mobileConnectionRepositoryFactory.build(subId)
+    }
+
+    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+        // Remove any connection repository from the cache that isn't in the new set of IDs. They
+        // will get garbage collected once their subscribers go away
+        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+        subIdRepositoryCache.keys.forEach {
+            if (!currentValidSubscriptionIds.contains(it)) {
+                subIdRepositoryCache.remove(it)
+            }
+        }
+    }
+
+    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
deleted file mode 100644
index 36de2a2..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrength
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
-
-/**
- * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
- * on various policy
- */
-interface MobileSubscriptionRepository {
-    /** Observable list of current mobile subscriptions */
-    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
-
-    /** Observable for the subscriptionId of the current mobile data connection */
-    val activeMobileDataSubscriptionId: Flow<Int>
-
-    /** Get or create an observable for the given subscription ID */
-    fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
-}
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MobileSubscriptionRepositoryImpl
-@Inject
-constructor(
-    private val subscriptionManager: SubscriptionManager,
-    private val telephonyManager: TelephonyManager,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    @Application private val scope: CoroutineScope,
-) : MobileSubscriptionRepository {
-    private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf()
-
-    /**
-     * State flow that emits the set of mobile data subscriptions, each represented by its own
-     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
-     * info object, but for now we keep track of the infos themselves.
-     */
-    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                        override fun onSubscriptionsChanged() {
-                            trySend(Unit)
-                        }
-                    }
-
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                    bgDispatcher.asExecutor(),
-                    callback,
-                )
-
-                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-            }
-            .mapLatest { fetchSubscriptionsList() }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
-
-    /** StateFlow that keeps track of the current active mobile data subscription */
-    override val activeMobileDataSubscriptionId: StateFlow<Int> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
-                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
-                            trySend(subId)
-                        }
-                    }
-
-                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
-            }
-            .stateIn(
-                scope,
-                started = SharingStarted.WhileSubscribed(),
-                SubscriptionManager.INVALID_SUBSCRIPTION_ID
-            )
-
-    /**
-     * Each mobile subscription needs its own flow, which comes from registering listeners on the
-     * system. Use this method to create those flows and cache them for reuse
-     */
-    override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> {
-        return subIdFlowCache[subId]
-            ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it }
-    }
-
-    @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache
-
-    private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run {
-        var state = MobileSubscriptionModel()
-        conflatedCallbackFlow {
-                val phony = telephonyManager.createForSubscriptionId(subId)
-                // TODO (b/240569788): log all of these into the connectivity logger
-                val callback =
-                    object :
-                        TelephonyCallback(),
-                        ServiceStateListener,
-                        SignalStrengthsListener,
-                        DataConnectionStateListener,
-                        DataActivityListener,
-                        CarrierNetworkListener,
-                        DisplayInfoListener {
-                        override fun onServiceStateChanged(serviceState: ServiceState) {
-                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
-                            trySend(state)
-                        }
-                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
-                            val cdmaLevel =
-                                signalStrength
-                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
-                                    .let { strengths ->
-                                        if (!strengths.isEmpty()) {
-                                            strengths[0].level
-                                        } else {
-                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
-                                        }
-                                    }
-
-                            val primaryLevel = signalStrength.level
-
-                            state =
-                                state.copy(
-                                    cdmaLevel = cdmaLevel,
-                                    primaryLevel = primaryLevel,
-                                    isGsm = signalStrength.isGsm,
-                                )
-                            trySend(state)
-                        }
-                        override fun onDataConnectionStateChanged(
-                            dataState: Int,
-                            networkType: Int
-                        ) {
-                            state = state.copy(dataConnectionState = dataState)
-                            trySend(state)
-                        }
-                        override fun onDataActivity(direction: Int) {
-                            state = state.copy(dataActivityDirection = direction)
-                            trySend(state)
-                        }
-                        override fun onCarrierNetworkChange(active: Boolean) {
-                            state = state.copy(carrierNetworkChangeActive = active)
-                            trySend(state)
-                        }
-                        override fun onDisplayInfoChanged(
-                            telephonyDisplayInfo: TelephonyDisplayInfo
-                        ) {
-                            state = state.copy(displayInfo = telephonyDisplayInfo)
-                            trySend(state)
-                        }
-                    }
-                phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose {
-                    phony.unregisterTelephonyCallback(callback)
-                    // Release the cached flow
-                    subIdFlowCache.remove(subId)
-                }
-            }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
-    }
-
-    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
-        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 40fe0f3..15f4acc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -17,32 +17,58 @@
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
 import android.telephony.CarrierConfigManager
-import com.android.settingslib.SignalIcon
-import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 interface MobileIconInteractor {
-    /** Identifier for RAT type indicator */
-    val iconGroup: Flow<SignalIcon.MobileIconGroup>
+    /** Observable for RAT type (network type) indicator */
+    val networkTypeIconGroup: Flow<MobileIconGroup>
+
     /** True if this line of service is emergency-only */
     val isEmergencyOnly: Flow<Boolean>
+
     /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
     val level: Flow<Int>
+
     /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
     val numberOfLevels: Flow<Int>
+
     /** True when we want to draw an icon that makes room for the exclamation mark */
     val cutOut: Flow<Boolean>
 }
 
 /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
 class MobileIconInteractorImpl(
-    mobileStatusInfo: Flow<MobileSubscriptionModel>,
+    defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
+    defaultMobileIconGroup: Flow<MobileIconGroup>,
+    mobileMappingsProxy: MobileMappingsProxy,
+    connectionRepository: MobileConnectionRepository,
 ) : MobileIconInteractor {
-    override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G)
+    private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
+
+    /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
+    override val networkTypeIconGroup: Flow<MobileIconGroup> =
+        combine(
+            mobileStatusInfo,
+            defaultMobileIconMapping,
+            defaultMobileIconGroup,
+        ) { info, mapping, defaultGroup ->
+            val lookupKey =
+                when (val resolved = info.resolvedNetworkType) {
+                    is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
+                    is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type)
+                }
+            mapping[lookupKey] ?: defaultGroup
+        }
+
     override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
 
     override val level: Flow<Int> =
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 8e67e19..cd411a4 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
@@ -19,29 +19,51 @@
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 /**
- * Business layer logic for mobile subscription icons
+ * Business layer logic for the set of mobile subscription icons.
  *
- * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s
- * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs
+ * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
+ * The list of subscriptions is filtered based on the opportunistic flags on the infos.
+ *
+ * It provides the default mapping between the telephony display info and the icon group that
+ * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
+ * icon
  */
+interface MobileIconsInteractor {
+    val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+    val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
+    val defaultMobileIconGroup: Flow<MobileIconGroup>
+    val isUserSetup: Flow<Boolean>
+    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
+}
+
 @SysUISingleton
-class MobileIconsInteractor
+class MobileIconsInteractorImpl
 @Inject
 constructor(
-    private val mobileSubscriptionRepo: MobileSubscriptionRepository,
+    private val mobileSubscriptionRepo: MobileConnectionsRepository,
     private val carrierConfigTracker: CarrierConfigTracker,
+    private val mobileMappingsProxy: MobileMappingsProxy,
     userSetupRepo: UserSetupRepository,
-) {
+    @Application private val scope: CoroutineScope,
+) : MobileIconsInteractor {
     private val activeMobileDataSubscriptionId =
         mobileSubscriptionRepo.activeMobileDataSubscriptionId
 
@@ -61,7 +83,7 @@
      * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
      * and by checking which subscription is opportunistic, or which one is active.
      */
-    val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+    override val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
         combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
             ->
             // Based on the old logic,
@@ -92,15 +114,29 @@
             }
         }
 
-    val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+    /**
+     * Mapping from network type to [MobileIconGroup] using the config generated for the default
+     * subscription Id. This mapping is the same for every subscription.
+     */
+    override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
+        mobileSubscriptionRepo.defaultDataSubRatConfig
+            .map { mobileMappingsProxy.mapIconSets(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
+
+    /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
+    override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
+        mobileSubscriptionRepo.defaultDataSubRatConfig
+            .map { mobileMappingsProxy.getDefaultIcons(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
+
+    override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
 
     /** Vends out new [MobileIconInteractor] for a particular subId */
-    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
-        MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId))
-
-    /**
-     * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections
-     */
-    private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
-        mobileSubscriptionRepo.getFlowForSubId(subId)
+    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+        MobileIconInteractorImpl(
+            defaultMobileIconMapping,
+            defaultMobileIconGroup,
+            mobileMappingsProxy,
+            mobileSubscriptionRepo.getRepoForSubId(subId),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 1405b05..67ea139 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.binder
 
 import android.content.res.ColorStateList
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.ImageView
 import androidx.core.view.isVisible
@@ -24,6 +26,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.R
+import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
 import kotlinx.coroutines.flow.collect
@@ -37,6 +40,7 @@
         view: ViewGroup,
         viewModel: MobileIconViewModel,
     ) {
+        val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
         val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
         val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
 
@@ -52,10 +56,20 @@
                     }
                 }
 
+                // Set the network type icon
+                launch {
+                    viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
+                        dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
+                        networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE
+                    }
+                }
+
                 // Set the tint
                 launch {
                     viewModel.tint.collect { tint ->
-                        iconView.imageTintList = ColorStateList.valueOf(tint)
+                        val tintList = ColorStateList.valueOf(tint)
+                        iconView.imageTintList = tintList
+                        networkTypeView.imageTintList = tintList
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index cfabeba..cc8f6dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -18,6 +18,8 @@
 
 import android.graphics.Color
 import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -26,6 +28,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
 /**
  * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -54,5 +57,15 @@
             .distinctUntilChanged()
             .logOutputChange(logger, "iconId($subscriptionId)")
 
+    /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+    var networkTypeIcon: Flow<Icon?> =
+        iconInteractor.networkTypeIconGroup.map {
+            val desc =
+                if (it.dataContentDescription != 0)
+                    ContentDescription.Resource(it.dataContentDescription)
+                else null
+            Icon.Resource(it.dataType, desc)
+        }
+
     var tint: Flow<Int> = flowOf(Color.CYAN)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
new file mode 100644
index 0000000..60bd038
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.util
+
+import android.telephony.Annotation.NetworkType
+import android.telephony.TelephonyDisplayInfo
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import javax.inject.Inject
+
+/**
+ * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to
+ * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This
+ * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake
+ * them out in tests
+ */
+interface MobileMappingsProxy {
+    fun mapIconSets(config: Config): Map<String, MobileIconGroup>
+    fun getDefaultIcons(config: Config): MobileIconGroup
+    fun toIconKey(@NetworkType networkType: Int): String
+    fun toIconKeyOverride(@NetworkType networkType: Int): String
+}
+
+/** Injectable wrapper class for [MobileMappings] */
+class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy {
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> =
+        MobileMappings.mapIconSets(config)
+
+    override fun getDefaultIcons(config: Config): MobileIconGroup =
+        MobileMappings.getDefaultIcons(config)
+
+    override fun toIconKey(@NetworkType networkType: Int): String =
+        MobileMappings.toIconKey(networkType)
+
+    override fun toIconKeyOverride(networkType: Int): String =
+        MobileMappings.toDisplayIconKey(networkType)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 681cf72..93448c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -39,7 +39,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import java.util.concurrent.Executor
@@ -64,6 +63,9 @@
     /** Observable for the current wifi enabled status. */
     val isWifiEnabled: StateFlow<Boolean>
 
+    /** Observable for the current wifi default status. */
+    val isWifiDefault: StateFlow<Boolean>
+
     /** Observable for the current wifi network. */
     val wifiNetwork: StateFlow<WifiNetworkModel>
 
@@ -103,7 +105,7 @@
             merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
                 .mapLatest { wifiManager.isWifiEnabled }
                 .distinctUntilChanged()
-                .logOutputChange(logger, "enabled")
+                .logInputChange(logger, "enabled")
                 .stateIn(
                     scope = scope,
                     started = SharingStarted.WhileSubscribed(),
@@ -111,6 +113,39 @@
                 )
         }
 
+    override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow {
+        // Note: This callback doesn't do any logging because we already log every network change
+        // in the [wifiNetwork] callback.
+        val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+            override fun onCapabilitiesChanged(
+                network: Network,
+                networkCapabilities: NetworkCapabilities
+            ) {
+                // This method will always be called immediately after the network becomes the
+                // default, in addition to any time the capabilities change while the network is
+                // the default.
+                // If this network contains valid wifi info, then wifi is the default network.
+                val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
+                trySend(wifiInfo != null)
+            }
+
+            override fun onLost(network: Network) {
+                // The system no longer has a default network, so wifi is definitely not default.
+                trySend(false)
+            }
+        }
+
+        connectivityManager.registerDefaultNetworkCallback(callback)
+        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+    }
+        .distinctUntilChanged()
+        .logInputChange(logger, "isWifiDefault")
+        .stateIn(
+            scope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = false
+        )
+
     override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow {
         var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 04b17ed..3a3e611 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -59,6 +59,9 @@
     /** Our current enabled status. */
     val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled
 
+    /** Our current default status. */
+    val isDefault: Flow<Boolean> = wifiRepository.isWifiDefault
+
     /** Our current wifi network. See [WifiNetworkModel]. */
     val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 273be63..25537b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -91,6 +91,7 @@
         val activityInView = view.requireViewById<ImageView>(R.id.wifi_in)
         val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out)
         val activityContainerView = view.requireViewById<View>(R.id.inout_container)
+        val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer)
 
         view.isVisible = true
         iconView.isVisible = true
@@ -142,6 +143,12 @@
                         activityContainerView.isVisible = visible
                     }
                 }
+
+                launch {
+                    viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible ->
+                        airplaneSpacer.isVisible = visible
+                    }
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
index 40f948f..95ab251 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
@@ -32,6 +32,7 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -40,4 +41,5 @@
         isActivityInViewVisible,
         isActivityOutViewVisible,
         isActivityContainerVisible,
+        isAirplaneSpacerVisible,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
index 9642ac4..86535d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
@@ -29,6 +29,7 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -37,4 +38,5 @@
         isActivityInViewVisible,
         isActivityOutViewVisible,
         isActivityContainerVisible,
+        isAirplaneSpacerVisible,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index e23f8c7..7cbdf5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -44,11 +44,14 @@
 
     /** True if the activity container view should be visible. */
     val isActivityContainerVisible: Flow<Boolean>,
+
+    /** True if the airplane spacer view should be visible. */
+    val isAirplaneSpacerVisible: Flow<Boolean>,
 ) {
     /** The color that should be used to tint the icon. */
     val tint: Flow<Int> =
         flowOf(
-            if (statusBarPipelineFlags.useNewPipelineDebugColoring()) {
+            if (statusBarPipelineFlags.useWifiDebugColoring()) {
                 debugTint
             } else {
                 DEFAULT_TINT
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
index 0ddf90e..fd54c5f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
@@ -29,6 +29,7 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -37,4 +38,5 @@
         isActivityInViewVisible,
         isActivityOutViewVisible,
         isActivityContainerVisible,
+        isAirplaneSpacerVisible,
     )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index ebbd77b..89b96b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
@@ -66,6 +67,7 @@
 class WifiViewModel
 @Inject
 constructor(
+    airplaneModeViewModel: AirplaneModeViewModel,
     connectivityConstants: ConnectivityConstants,
     private val context: Context,
     logger: ConnectivityPipelineLogger,
@@ -124,9 +126,10 @@
     private val wifiIcon: StateFlow<Icon.Resource?> =
         combine(
             interactor.isEnabled,
+            interactor.isDefault,
             interactor.isForceHidden,
             interactor.wifiNetwork,
-        ) { isEnabled, isForceHidden, wifiNetwork ->
+        ) { isEnabled, isDefault, isForceHidden, wifiNetwork ->
             if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
                 return@combine null
             }
@@ -135,6 +138,7 @@
             val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription())
 
             return@combine when {
+                isDefault -> icon
                 wifiConstants.alwaysShowIconIfEnabled -> icon
                 !connectivityConstants.hasDataCapabilities -> icon
                 wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
@@ -175,6 +179,12 @@
                 }
              .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
 
+    // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the
+    //  airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel
+    //  that appropriately knows about both icons and sets the padding appropriately.
+    private val isAirplaneSpacerVisible: Flow<Boolean> =
+        airplaneModeViewModel.isAirplaneModeIconVisible
+
     /** A view model for the status bar on the home screen. */
     val home: HomeWifiViewModel =
         HomeWifiViewModel(
@@ -183,6 +193,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     /** A view model for the status bar on keyguard. */
@@ -193,6 +204,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     /** A view model for the status bar in quick settings. */
@@ -203,6 +215,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 28a9b97..cf4106c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -61,7 +61,7 @@
      * animation to and from the parent dialog.
      */
     @JvmOverloads
-    fun onUserListItemClicked(
+    open fun onUserListItemClicked(
         record: UserRecord,
         dialogShower: DialogShower? = null,
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index dc73d1f..f63d652 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -36,6 +36,7 @@
 import com.android.keyguard.dagger.KeyguardUserSwitcherScope;
 import com.android.settingslib.drawable.CircleFramedDrawable;
 import com.android.systemui.R;
+import com.android.systemui.animation.Expandable;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -190,7 +191,8 @@
             mUiEventLogger.log(
                     LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP);
 
-            mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground);
+            mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(),
+                    Expandable.fromView(mUserAvatarViewWithBackground));
         });
 
         mUserAvatarView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index da6d455..dd400b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
 import android.view.View;
 import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
+import android.view.ViewRootImpl;
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsController;
@@ -61,6 +62,8 @@
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -88,6 +91,7 @@
  */
 public class RemoteInputView extends LinearLayout implements View.OnClickListener {
 
+    private static final boolean DEBUG = false;
     private static final String TAG = "RemoteInput";
 
     // A marker object that let's us easily find views of this class.
@@ -124,6 +128,7 @@
     // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
     //  that need the controller shouldn't have access to the view
     private RemoteInputViewController mViewController;
+    private ViewRootImpl mTestableViewRootImpl;
 
     /**
      * Enum for logged notification remote input UiEvents.
@@ -430,10 +435,20 @@
         }
     }
 
+    @VisibleForTesting
+    protected void setViewRootImpl(ViewRootImpl viewRoot) {
+        mTestableViewRootImpl = viewRoot;
+    }
+
+    @VisibleForTesting
+    protected void setEditTextReferenceToSelf() {
+        mEditText.mRemoteInputView = this;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mEditText.mRemoteInputView = this;
+        setEditTextReferenceToSelf();
         mEditText.setOnEditorActionListener(mEditorActionHandler);
         mEditText.addTextChangedListener(mTextWatcher);
         if (mEntry.getRow().isChangingPosition()) {
@@ -457,7 +472,50 @@
     }
 
     @Override
+    public ViewRootImpl getViewRootImpl() {
+        if (mTestableViewRootImpl != null) {
+            return mTestableViewRootImpl;
+        }
+        return super.getViewRootImpl();
+    }
+
+    private void registerBackCallback() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            if (DEBUG) {
+                Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "registering Predictive Back callback");
+        }
+        viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback);
+    }
+
+    private void unregisterBackCallback() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            if (DEBUG) {
+                Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "unregistering Predictive Back callback");
+        }
+        viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(
+                mEditText.mOnBackInvokedCallback);
+    }
+
+    @Override
     public void onVisibilityAggregated(boolean isVisible) {
+        if (isVisible) {
+            registerBackCallback();
+        } else {
+            unregisterBackCallback();
+        }
         super.onVisibilityAggregated(isVisible);
         mEditText.setEnabled(isVisible && !mSending);
     }
@@ -822,10 +880,21 @@
             return super.onKeyDown(keyCode, event);
         }
 
+        private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
+            if (DEBUG) {
+                Log.d(TAG, "Predictive Back Callback dispatched");
+            }
+            respondToKeycodeBack();
+        };
+
+        private void respondToKeycodeBack() {
+            defocusIfNeeded(true /* animate */);
+        }
+
         @Override
         public boolean onKeyUp(int keyCode, KeyEvent event) {
             if (keyCode == KeyEvent.KEYCODE_BACK) {
-                defocusIfNeeded(true /* animate */);
+                respondToKeycodeBack();
                 return true;
             }
             return super.onKeyUp(keyCode, event);
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index d5d904c..f0a50de 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.temporarydisplay
 
 import android.annotation.LayoutRes
-import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
@@ -67,11 +66,10 @@
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
      * all subclasses.
      */
-    @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
     internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
         width = WindowManager.LayoutParams.WRAP_CONTENT
         height = WindowManager.LayoutParams.WRAP_CONTENT
-        type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
+        type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
         flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
             WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
         title = windowTitle
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 1a25e4d..b8930a4 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -18,7 +18,6 @@
 
 import android.content.Context
 import android.graphics.Rect
-import android.media.MediaRoute2Info
 import android.os.PowerManager
 import android.view.Gravity
 import android.view.MotionEvent
@@ -27,25 +26,25 @@
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.ViewHierarchyAnimator
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.common.ui.binder.TextViewBinder
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttUtils
-import com.android.systemui.media.taptotransfer.sender.ChipStateSender
 import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
-import com.android.systemui.media.taptotransfer.sender.TransferStatus
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
-import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
 import javax.inject.Inject
@@ -78,11 +77,11 @@
         accessibilityManager: AccessibilityManager,
         configurationController: ConfigurationController,
         powerManager: PowerManager,
-        private val uiEventLogger: MediaTttSenderUiEventLogger,
         private val falsingManager: FalsingManager,
         private val falsingCollector: FalsingCollector,
         private val viewUtil: ViewUtil,
-) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
+        private val vibratorHelper: VibratorHelper,
+) : TemporaryViewDisplayController<ChipbarInfo, MediaTttLogger>(
         context,
         logger,
         windowManager,
@@ -104,15 +103,13 @@
     override fun start() {}
 
     override fun updateView(
-        newInfo: ChipSenderInfo,
+        newInfo: ChipbarInfo,
         currentView: ViewGroup
     ) {
         // TODO(b/245610654): Adding logging here.
 
-        val chipState = newInfo.state
-
         // Detect falsing touches on the chip.
-        parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
+        parent = currentView.requireViewById(R.id.chipbar_root_view)
         parent.touchHandler = object : Gefingerpoken {
             override fun onTouchEvent(ev: MotionEvent?): Boolean {
                 falsingCollector.onTouchEvent(ev)
@@ -120,47 +117,57 @@
             }
         }
 
-        // App icon
-        val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
-            context, newInfo.routeInfo.clientPackageName, logger
-        )
-        val iconView = currentView.requireViewById<CachingIconView>(R.id.app_icon)
-        iconView.setImageDrawable(iconInfo.drawable)
-        iconView.contentDescription = iconInfo.contentDescription
+        // ---- Start icon ----
+        val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon)
+        IconViewBinder.bind(newInfo.startIcon, iconView)
 
-        // Text
-        val otherDeviceName = newInfo.routeInfo.name.toString()
-        val chipText = chipState.getChipTextString(context, otherDeviceName)
-        currentView.requireViewById<TextView>(R.id.text).text = chipText
+        // ---- Text ----
+        val textView = currentView.requireViewById<TextView>(R.id.text)
+        TextViewBinder.bind(textView, newInfo.text)
+        // Updates text view bounds to make sure it perfectly fits the new text
+        // (If the new text is smaller than the previous text) see b/253228632.
+        textView.requestLayout()
 
+        // ---- End item ----
         // Loading
         currentView.requireViewById<View>(R.id.loading).visibility =
-            (chipState.transferStatus == TransferStatus.IN_PROGRESS).visibleIfTrue()
+            (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue()
 
-        // Undo
-        val undoView = currentView.requireViewById<View>(R.id.undo)
-        val undoClickListener = chipState.undoClickListener(
-                this,
-                newInfo.routeInfo,
-                newInfo.undoCallback,
-                uiEventLogger,
-                falsingManager,
-        )
-        undoView.setOnClickListener(undoClickListener)
-        undoView.visibility = (undoClickListener != null).visibleIfTrue()
+        // Error
+        currentView.requireViewById<View>(R.id.error).visibility =
+            (newInfo.endItem == ChipbarEndItem.Error).visibleIfTrue()
 
-        // Failure
-        currentView.requireViewById<View>(R.id.failure_icon).visibility =
-            (chipState.transferStatus == TransferStatus.FAILED).visibleIfTrue()
+        // Button
+        val buttonView = currentView.requireViewById<TextView>(R.id.end_button)
+        if (newInfo.endItem is ChipbarEndItem.Button) {
+            TextViewBinder.bind(buttonView, newInfo.endItem.text)
 
-        // For accessibility
+            val onClickListener = View.OnClickListener { clickedView ->
+                if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+                newInfo.endItem.onClickListener.onClick(clickedView)
+            }
+
+            buttonView.setOnClickListener(onClickListener)
+            buttonView.visibility = View.VISIBLE
+        } else {
+            buttonView.visibility = View.GONE
+        }
+
+        // ---- Overall accessibility ----
         currentView.requireViewById<ViewGroup>(
-                R.id.media_ttt_sender_chip_inner
-        ).contentDescription = "${iconInfo.contentDescription} $chipText"
+                R.id.chipbar_inner
+        ).contentDescription =
+            "${newInfo.startIcon.contentDescription.loadContentDescription(context)} " +
+                "${newInfo.text.loadText(context)}"
+
+        // ---- Haptics ----
+        newInfo.vibrationEffect?.let {
+            vibratorHelper.vibrate(it)
+        }
     }
 
     override fun animateViewIn(view: ViewGroup) {
-        val chipInnerView = view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner)
+        val chipInnerView = view.requireViewById<ViewGroup>(R.id.chipbar_inner)
         ViewHierarchyAnimator.animateAddition(
             chipInnerView,
             ViewHierarchyAnimator.Hotspot.TOP,
@@ -175,7 +182,7 @@
 
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         ViewHierarchyAnimator.animateRemoval(
-            view.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
+            view.requireViewById<ViewGroup>(R.id.chipbar_inner),
             ViewHierarchyAnimator.Hotspot.TOP,
             Interpolators.EMPHASIZED_ACCELERATE,
             ANIMATION_DURATION,
@@ -197,13 +204,4 @@
     }
 }
 
-data class ChipSenderInfo(
-    val state: ChipStateSender,
-    val routeInfo: MediaRoute2Info,
-    val undoCallback: IUndoMediaTransferCallback? = null
-) : TemporaryViewInfo {
-    override fun getTimeoutMs() = state.timeout
-}
-
-const val SENDER_TAG = "MediaTapToTransferSender"
 private const val ANIMATION_DURATION = 500L
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
new file mode 100644
index 0000000..57fde87
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.temporarydisplay.chipbar
+
+import android.os.VibrationEffect
+import android.view.View
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.temporarydisplay.TemporaryViewInfo
+
+/**
+ * A container for all the state needed to display a chipbar via [ChipbarCoordinator].
+ *
+ * @property startIcon the icon to display at the start of the chipbar (on the left in LTR locales;
+ * on the right in RTL locales).
+ * @property text the text to display.
+ * @property endItem an optional end item to display at the end of the chipbar (on the right in LTR
+ * locales; on the left in RTL locales).
+ * @property vibrationEffect an optional vibration effect when the chipbar is displayed
+ */
+data class ChipbarInfo(
+    val startIcon: Icon,
+    val text: Text,
+    val endItem: ChipbarEndItem?,
+    val vibrationEffect: VibrationEffect? = null,
+) : TemporaryViewInfo
+
+/** The possible items to display at the end of the chipbar. */
+sealed class ChipbarEndItem {
+    /** A loading icon should be displayed. */
+    object Loading : ChipbarEndItem()
+
+    /** An error icon should be displayed. */
+    object Error : ChipbarEndItem()
+
+    /**
+     * A button with the provided [text] and [onClickListener] functionality should be displayed.
+     */
+    data class Button(val text: Text, val onClickListener: View.OnClickListener) : ChipbarEndItem()
+
+    // TODO(b/245610654): Add support for a generic icon.
+}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 3d56f23..3ecb15b 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -79,6 +79,7 @@
 import org.json.JSONObject;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
@@ -114,6 +115,7 @@
     private final SecureSettings mSecureSettings;
     private final Executor mMainExecutor;
     private final Handler mBgHandler;
+    private final boolean mIsMonochromaticEnabled;
     private final Context mContext;
     private final boolean mIsMonetEnabled;
     private final UserTracker mUserTracker;
@@ -363,6 +365,7 @@
             UserTracker userTracker, DumpManager dumpManager, FeatureFlags featureFlags,
             @Main Resources resources, WakefulnessLifecycle wakefulnessLifecycle) {
         mContext = context;
+        mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEMES);
         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
         mDeviceProvisionedController = deviceProvisionedController;
         mBroadcastDispatcher = broadcastDispatcher;
@@ -665,8 +668,13 @@
         // Allow-list of Style objects that can be created from a setting string, i.e. can be
         // used as a system-wide theme.
         // - Content intentionally excluded, intended for media player, not system-wide
-        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
-                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
+        List<Style> validStyles = new ArrayList<>(Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ,
+                Style.TONAL_SPOT, Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT));
+
+        if (mIsMonochromaticEnabled) {
+            validStyles.add(Style.MONOCHROMATIC);
+        }
+
         Style style = mThemeStyle;
         final String overlayPackageJson = mSecureSettings.getStringForUser(
                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
index ee785b6..088cd93 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt
@@ -36,9 +36,7 @@
     private var adapter: ListAdapter? = null
 
     init {
-        setBackgroundDrawable(
-            res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme())
-        )
+        setBackgroundDrawable(null)
         setModal(false)
         setOverlapAnchor(true)
     }
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 919e699..b16dc54 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
@@ -220,7 +220,12 @@
             val result = withContext(backgroundDispatcher) { manager.aliveUsers }
 
             if (result != null) {
-                _userInfos.value = result.sortedBy { it.creationTime }
+                _userInfos.value =
+                    result
+                        // Users should be sorted by ascending creation time.
+                        .sortedBy { it.creationTime }
+                        // The guest user is always last, regardless of creation time.
+                        .sortedBy { it.isGuest }
             }
         }
     }
@@ -321,6 +326,7 @@
         return when {
             isAddUser -> false
             isAddSupervisedUser -> false
+            isManageUsers -> false
             isGuest -> info != null
             else -> true
         }
@@ -346,6 +352,7 @@
             isAddUser -> UserActionModel.ADD_USER
             isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
             isGuest -> UserActionModel.ENTER_GUEST_MODE
+            isManageUsers -> UserActionModel.NAVIGATE_TO_USER_MANAGEMENT
             else -> error("Don't know how to convert to UserActionModel: $this")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index ba5a82a..dda78aa 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -236,18 +236,7 @@
                     }
                     .flatMapLatest { isActionable ->
                         if (isActionable) {
-                            repository.actions.map { actions ->
-                                actions +
-                                    if (actions.isNotEmpty()) {
-                                        // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
-                                        // because that's a user switcher specific action that is
-                                        // not known to the our data source or other features.
-                                        listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                                    } else {
-                                        // If no actions, don't add the navigate action.
-                                        emptyList()
-                                    }
-                            }
+                            repository.actions
                         } else {
                             // If not actionable it means that we're not allowed to show actions
                             // when
@@ -440,6 +429,7 @@
                         isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
                         isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
                         onExitGuestUser = this::exitGuestUser,
+                        dialogShower = dialogShower,
                     )
                 )
                 return
@@ -454,6 +444,7 @@
                         isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
                         isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
                         onExitGuestUser = this::exitGuestUser,
+                        dialogShower = dialogShower,
                     )
                 )
                 return
@@ -488,6 +479,7 @@
                             userHandle = currentUser.userHandle,
                             isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
                             showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+                            dialogShower = dialogShower,
                         )
                     )
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
index 08d7c5a..177356e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -18,14 +18,18 @@
 package com.android.systemui.user.domain.model
 
 import android.os.UserHandle
+import com.android.systemui.qs.user.UserSwitchDialogController
 
 /** Encapsulates a request to show a dialog. */
-sealed class ShowDialogRequestModel {
+sealed class ShowDialogRequestModel(
+    open val dialogShower: UserSwitchDialogController.DialogShower? = null,
+) {
     data class ShowAddUserDialog(
         val userHandle: UserHandle,
         val isKeyguardShowing: Boolean,
         val showEphemeralMessage: Boolean,
-    ) : ShowDialogRequestModel()
+        override val dialogShower: UserSwitchDialogController.DialogShower?,
+    ) : ShowDialogRequestModel(dialogShower)
 
     data class ShowUserCreationDialog(
         val isGuest: Boolean,
@@ -37,5 +41,6 @@
         val isGuestEphemeral: Boolean,
         val isKeyguardShowing: Boolean,
         val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
-    ) : ShowDialogRequestModel()
+        override val dialogShower: UserSwitchDialogController.DialogShower?,
+    ) : ShowDialogRequestModel(dialogShower)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
index 938417f..968af59 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -18,12 +18,15 @@
 package com.android.systemui.user.ui.binder
 
 import android.content.Context
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.widget.BaseAdapter
 import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.LinearLayout.SHOW_DIVIDER_MIDDLE
 import android.widget.TextView
 import androidx.constraintlayout.helper.widget.Flow as FlowWidget
 import androidx.core.view.isVisible
@@ -36,6 +39,7 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.user.UserSwitcherPopupMenu
 import com.android.systemui.user.UserSwitcherRootView
+import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.ui.viewmodel.UserActionViewModel
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
 import com.android.systemui.util.children
@@ -168,15 +172,10 @@
         onDismissed: () -> Unit,
     ): UserSwitcherPopupMenu {
         return UserSwitcherPopupMenu(context).apply {
+            this.setDropDownGravity(Gravity.END)
             this.anchorView = anchorView
             setAdapter(adapter)
             setOnDismissListener { onDismissed() }
-            setOnItemClickListener { _, _, position, _ ->
-                val itemPositionExcludingHeader = position - 1
-                adapter.getItem(itemPositionExcludingHeader).onClicked()
-                dismiss()
-            }
-
             show()
         }
     }
@@ -186,38 +185,67 @@
         private val layoutInflater: LayoutInflater,
     ) : BaseAdapter() {
 
-        private val items = mutableListOf<UserActionViewModel>()
+        private var sections = listOf<List<UserActionViewModel>>()
 
         override fun getCount(): Int {
-            return items.size
+            return sections.size
         }
 
-        override fun getItem(position: Int): UserActionViewModel {
-            return items[position]
+        override fun getItem(position: Int): List<UserActionViewModel> {
+            return sections[position]
         }
 
         override fun getItemId(position: Int): Long {
-            return getItem(position).viewKey
+            return position.toLong()
         }
 
         override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
-            val view =
-                convertView
-                    ?: layoutInflater.inflate(
+            val section = getItem(position)
+            val context = parent.context
+            val sectionView =
+                convertView as? LinearLayout
+                    ?: LinearLayout(context, null).apply {
+                        this.orientation = LinearLayout.VERTICAL
+                        this.background =
+                            parent.resources.getDrawable(
+                                R.drawable.bouncer_user_switcher_popup_bg,
+                                context.theme
+                            )
+                        this.showDividers = SHOW_DIVIDER_MIDDLE
+                        this.dividerDrawable =
+                            context.getDrawable(
+                                R.drawable.fullscreen_userswitcher_menu_item_divider
+                            )
+                    }
+            sectionView.removeAllViewsInLayout()
+
+            for (viewModel in section) {
+                val view =
+                    layoutInflater.inflate(
                         R.layout.user_switcher_fullscreen_popup_item,
-                        parent,
-                        false
+                        /* parent= */ null
                     )
-            val viewModel = getItem(position)
-            view.requireViewById<ImageView>(R.id.icon).setImageResource(viewModel.iconResourceId)
-            view.requireViewById<TextView>(R.id.text).text =
-                view.resources.getString(viewModel.textResourceId)
-            return view
+                view
+                    .requireViewById<ImageView>(R.id.icon)
+                    .setImageResource(viewModel.iconResourceId)
+                view.requireViewById<TextView>(R.id.text).text =
+                    view.resources.getString(viewModel.textResourceId)
+                view.setOnClickListener { viewModel.onClicked() }
+                sectionView.addView(view)
+            }
+            return sectionView
         }
 
         fun setItems(items: List<UserActionViewModel>) {
-            this.items.clear()
-            this.items.addAll(items)
+            val primarySection =
+                items.filter {
+                    it.viewKey != UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
+                }
+            val secondarySection =
+                items.filter {
+                    it.viewKey == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong()
+                }
+            this.sections = listOf(primarySection, secondarySection)
             notifyDataSetChanged()
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
index 91c5921..e921720 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -19,8 +19,10 @@
 
 import android.app.Dialog
 import android.content.Context
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.settingslib.users.UserCreatingDialog
 import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogCuj
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dagger.SysUISingleton
@@ -30,6 +32,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.user.domain.interactor.UserInteractor
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.collect
@@ -41,19 +44,19 @@
 class UserSwitcherDialogCoordinator
 @Inject
 constructor(
-    @Application private val context: Context,
-    @Application private val applicationScope: CoroutineScope,
-    private val falsingManager: FalsingManager,
-    private val broadcastSender: BroadcastSender,
-    private val dialogLaunchAnimator: DialogLaunchAnimator,
-    private val interactor: UserInteractor,
-    private val featureFlags: FeatureFlags,
+    @Application private val context: Lazy<Context>,
+    @Application private val applicationScope: Lazy<CoroutineScope>,
+    private val falsingManager: Lazy<FalsingManager>,
+    private val broadcastSender: Lazy<BroadcastSender>,
+    private val dialogLaunchAnimator: Lazy<DialogLaunchAnimator>,
+    private val interactor: Lazy<UserInteractor>,
+    private val featureFlags: Lazy<FeatureFlags>,
 ) : CoreStartable {
 
     private var currentDialog: Dialog? = null
 
     override fun start() {
-        if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+        if (featureFlags.get().isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
             return
         }
 
@@ -62,61 +65,87 @@
     }
 
     private fun startHandlingDialogShowRequests() {
-        applicationScope.launch {
-            interactor.dialogShowRequests.filterNotNull().collect { request ->
+        applicationScope.get().launch {
+            interactor.get().dialogShowRequests.filterNotNull().collect { request ->
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                currentDialog =
+                val (dialog, dialogCuj) =
                     when (request) {
                         is ShowDialogRequestModel.ShowAddUserDialog ->
-                            AddUserDialog(
-                                context = context,
-                                userHandle = request.userHandle,
-                                isKeyguardShowing = request.isKeyguardShowing,
-                                showEphemeralMessage = request.showEphemeralMessage,
-                                falsingManager = falsingManager,
-                                broadcastSender = broadcastSender,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
+                            Pair(
+                                AddUserDialog(
+                                    context = context.get(),
+                                    userHandle = request.userHandle,
+                                    isKeyguardShowing = request.isKeyguardShowing,
+                                    showEphemeralMessage = request.showEphemeralMessage,
+                                    falsingManager = falsingManager.get(),
+                                    broadcastSender = broadcastSender.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_ADD_NEW_USER_TAG,
+                                ),
                             )
                         is ShowDialogRequestModel.ShowUserCreationDialog ->
-                            UserCreatingDialog(
-                                context,
-                                request.isGuest,
+                            Pair(
+                                UserCreatingDialog(
+                                    context.get(),
+                                    request.isGuest,
+                                ),
+                                null,
                             )
                         is ShowDialogRequestModel.ShowExitGuestDialog ->
-                            ExitGuestDialog(
-                                context = context,
-                                guestUserId = request.guestUserId,
-                                isGuestEphemeral = request.isGuestEphemeral,
-                                targetUserId = request.targetUserId,
-                                isKeyguardShowing = request.isKeyguardShowing,
-                                falsingManager = falsingManager,
-                                dialogLaunchAnimator = dialogLaunchAnimator,
-                                onExitGuestUserListener = request.onExitGuestUser,
+                            Pair(
+                                ExitGuestDialog(
+                                    context = context.get(),
+                                    guestUserId = request.guestUserId,
+                                    isGuestEphemeral = request.isGuestEphemeral,
+                                    targetUserId = request.targetUserId,
+                                    isKeyguardShowing = request.isKeyguardShowing,
+                                    falsingManager = falsingManager.get(),
+                                    dialogLaunchAnimator = dialogLaunchAnimator.get(),
+                                    onExitGuestUserListener = request.onExitGuestUser,
+                                ),
+                                DialogCuj(
+                                    InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
+                                    INTERACTION_JANK_EXIT_GUEST_MODE_TAG,
+                                ),
                             )
                     }
+                currentDialog = dialog
 
-                currentDialog?.show()
-                interactor.onDialogShown()
+                if (request.dialogShower != null && dialogCuj != null) {
+                    request.dialogShower?.showDialog(dialog, dialogCuj)
+                } else {
+                    dialog.show()
+                }
+
+                interactor.get().onDialogShown()
             }
         }
     }
 
     private fun startHandlingDialogDismissRequests() {
-        applicationScope.launch {
-            interactor.dialogDismissRequests.filterNotNull().collect {
+        applicationScope.get().launch {
+            interactor.get().dialogDismissRequests.filterNotNull().collect {
                 currentDialog?.let {
                     if (it.isShowing) {
                         it.cancel()
                     }
                 }
 
-                interactor.onDialogDismissed()
+                interactor.get().onDialogDismissed()
             }
         }
     }
+
+    companion object {
+        private const val INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user"
+        private const val INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 219dae2..d857e85 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -62,17 +62,7 @@
     val isMenuVisible: Flow<Boolean> = _isMenuVisible
     /** The user action menu. */
     val menu: Flow<List<UserActionViewModel>> =
-        userInteractor.actions.map { actions ->
-            if (isNewImpl && actions.isNotEmpty()) {
-                    // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
-                    // switcher specific action that is not known to the our data source or other
-                    // features.
-                    actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-                } else {
-                    actions
-                }
-                .map { action -> toViewModel(action) }
-        }
+        userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
 
     /** Whether the button to open the user action menu is visible. */
     val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
index ecb365f..2c317dd 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Condition.java
@@ -172,10 +172,14 @@
         return Boolean.TRUE.equals(mIsConditionMet);
     }
 
-    private boolean shouldLog() {
+    protected final boolean shouldLog() {
         return Log.isLoggable(mTag, Log.DEBUG);
     }
 
+    protected final String getTag() {
+        return mTag;
+    }
+
     /**
      * Callback that receives updates about whether the condition has been fulfilled.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index 4824f67..cb430ba 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -117,6 +117,7 @@
         final SubscriptionState state = new SubscriptionState(subscription);
 
         mExecutor.execute(() -> {
+            if (shouldLog()) Log.d(mTag, "adding subscription");
             mSubscriptions.put(token, state);
 
             // Add and associate conditions.
@@ -143,7 +144,7 @@
      */
     public void removeSubscription(@NotNull Subscription.Token token) {
         mExecutor.execute(() -> {
-            if (shouldLog()) Log.d(mTag, "removing callback");
+            if (shouldLog()) Log.d(mTag, "removing subscription");
             if (!mSubscriptions.containsKey(token)) {
                 Log.e(mTag, "subscription not present:" + token);
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
new file mode 100644
index 0000000..b7166d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/proto/component_name.proto
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package com.android.systemui.util;
+
+option java_multiple_files = true;
+
+message ComponentNameProto {
+  string package_name = 1;
+  string class_name = 2;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index fbc6a58..309f168 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -55,6 +56,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.nano.WmShellTraceProto;
 import com.android.wm.shell.onehanded.OneHanded;
@@ -111,6 +114,7 @@
     private final Optional<SplitScreen> mSplitScreenOptional;
     private final Optional<OneHanded> mOneHandedOptional;
     private final Optional<FloatingTasks> mFloatingTasksOptional;
+    private final Optional<DesktopMode> mDesktopModeOptional;
 
     private final CommandQueue mCommandQueue;
     private final ConfigurationController mConfigurationController;
@@ -173,6 +177,7 @@
             Optional<SplitScreen> splitScreenOptional,
             Optional<OneHanded> oneHandedOptional,
             Optional<FloatingTasks> floatingTasksOptional,
+            Optional<DesktopMode> desktopMode,
             CommandQueue commandQueue,
             ConfigurationController configurationController,
             KeyguardStateController keyguardStateController,
@@ -194,6 +199,7 @@
         mPipOptional = pipOptional;
         mSplitScreenOptional = splitScreenOptional;
         mOneHandedOptional = oneHandedOptional;
+        mDesktopModeOptional = desktopMode;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
         mUserTracker = userTracker;
@@ -219,6 +225,7 @@
         mPipOptional.ifPresent(this::initPip);
         mSplitScreenOptional.ifPresent(this::initSplitScreen);
         mOneHandedOptional.ifPresent(this::initOneHanded);
+        mDesktopModeOptional.ifPresent(this::initDesktopMode);
     }
 
     @VisibleForTesting
@@ -326,6 +333,16 @@
         });
     }
 
+    void initDesktopMode(DesktopMode desktopMode) {
+        desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() {
+            @Override
+            public void onVisibilityChanged(boolean hasFreeformTasks) {
+                mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
+                        .commitUpdate(DEFAULT_DISPLAY);
+            }
+        }, mSysUiMainExecutor);
+    }
+
     @Override
     public void writeToProto(SystemUiTraceProto proto) {
         if (proto.wmShell == null) {
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 0fa3d36..c55ee61 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -88,6 +88,11 @@
                   android:excludeFromRecents="true"
                   />
 
+        <activity android:name=".settings.brightness.BrightnessDialogTest$TestDialog"
+            android:exported="false"
+            android:excludeFromRecents="true"
+            />
+
         <activity android:name="com.android.systemui.screenshot.ScrollViewActivity"
                   android:exported="false" />
 
diff --git a/packages/SystemUI/tests/res/layout/custom_view_dark.xml b/packages/SystemUI/tests/res/layout/custom_view_dark.xml
index 9e460a5..112d73d2 100644
--- a/packages/SystemUI/tests/res/layout/custom_view_dark.xml
+++ b/packages/SystemUI/tests/res/layout/custom_view_dark.xml
@@ -14,6 +14,7 @@
     limitations under the License.
 -->
 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/custom_view_dark_image"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="#ff000000"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
index 9d6aff2..7b9b39f 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/BouncerKeyguardMessageAreaTest.kt
@@ -66,4 +66,13 @@
         underTest.setMessage(null)
         assertThat(underTest.text).isEqualTo("")
     }
+
+    @Test
+    fun testSetNullClearsPreviousMessage() {
+        underTest.setMessage("something not null")
+        assertThat(underTest.text).isEqualTo("something not null")
+
+        underTest.setMessage(null)
+        assertThat(underTest.text).isEqualTo("")
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 8a2c354..1c3656d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -17,17 +17,22 @@
 
 import android.content.BroadcastReceiver
 import android.testing.AndroidTestingRunner
+import android.view.View
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.plugins.ClockAnimations
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
-import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.mockito.any
@@ -37,6 +42,9 @@
 import com.android.systemui.util.mockito.mock
 import java.util.TimeZone
 import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Rule
@@ -57,7 +65,7 @@
 class ClockEventControllerTest : SysuiTestCase() {
 
     @JvmField @Rule val mockito = MockitoJUnit.rule()
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var keyguardInteractor: KeyguardInteractor
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var batteryController: BatteryController
     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -72,8 +80,11 @@
     @Mock private lateinit var largeClockController: ClockFaceController
     @Mock private lateinit var smallClockEvents: ClockFaceEvents
     @Mock private lateinit var largeClockEvents: ClockFaceEvents
-
-    private lateinit var clockEventController: ClockEventController
+    @Mock private lateinit var parentView: View
+    @Mock private lateinit var transitionRepository: KeyguardTransitionRepository
+    private lateinit var repository: FakeKeyguardRepository
+    @Mock private lateinit var logBuffer: LogBuffer
+    private lateinit var underTest: ClockEventController
 
     @Before
     fun setUp() {
@@ -86,8 +97,11 @@
         whenever(clock.events).thenReturn(events)
         whenever(clock.animations).thenReturn(animations)
 
-        clockEventController = ClockEventController(
-            statusBarStateController,
+        repository = FakeKeyguardRepository()
+
+        underTest = ClockEventController(
+            KeyguardInteractor(repository = repository),
+            KeyguardTransitionInteractor(repository = transitionRepository),
             broadcastDispatcher,
             batteryController,
             keyguardUpdateMonitor,
@@ -96,33 +110,36 @@
             context,
             mainExecutor,
             bgExecutor,
+            logBuffer,
             featureFlags
         )
+        underTest.clock = clock
+
+        runBlocking(IMMEDIATE) {
+            underTest.registerListeners(parentView)
+
+            repository.setDozing(true)
+            repository.setDozeAmount(1f)
+        }
     }
 
     @Test
     fun clockSet_validateInitialization() {
-        clockEventController.clock = clock
-
         verify(clock).initialize(any(), anyFloat(), anyFloat())
     }
 
     @Test
     fun clockUnset_validateState() {
-        clockEventController.clock = clock
-        clockEventController.clock = null
+        underTest.clock = null
 
-        assertEquals(clockEventController.clock, null)
+        assertEquals(underTest.clock, null)
     }
 
     @Test
-    fun themeChanged_verifyClockPaletteUpdated() {
-        clockEventController.clock = clock
+    fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) {
         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onThemeChanged()
@@ -131,13 +148,10 @@
     }
 
     @Test
-    fun fontChanged_verifyFontSizeUpdated() {
-        clockEventController.clock = clock
+    fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) {
         verify(smallClockEvents).onRegionDarknessChanged(anyBoolean())
         verify(largeClockEvents).onRegionDarknessChanged(anyBoolean())
 
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
         verify(configurationController).addCallback(capture(captor))
         captor.value.onDensityOrFontScaleChanged()
@@ -146,10 +160,7 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
         val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
         verify(batteryController).addCallback(capture(batteryCaptor))
         val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -161,26 +172,21 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() =
+        runBlocking(IMMEDIATE) {
+            val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(batteryCaptor))
+            val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+            keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, true)
 
-        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
-        verify(batteryController).addCallback(capture(batteryCaptor))
-        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
-        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
-        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
-
-        verify(animations, times(1)).charge()
-    }
+            verify(animations, times(1)).charge()
+        }
 
     @Test
-    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() = runBlocking(IMMEDIATE) {
         val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
         verify(batteryController).addCallback(capture(batteryCaptor))
         val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
@@ -192,25 +198,20 @@
     }
 
     @Test
-    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() =
+        runBlocking(IMMEDIATE) {
+            val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+            verify(batteryController).addCallback(capture(batteryCaptor))
+            val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+            verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+            keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+            batteryCaptor.value.onBatteryLevelChanged(10, false, false)
 
-        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
-        verify(batteryController).addCallback(capture(batteryCaptor))
-        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
-        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
-        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
-        batteryCaptor.value.onBatteryLevelChanged(10, false, false)
-
-        verify(animations, never()).charge()
-    }
+            verify(animations, never()).charge()
+        }
 
     @Test
-    fun localeCallback_verifyClockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun localeCallback_verifyClockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<BroadcastReceiver>()
         verify(broadcastDispatcher).registerReceiver(
             capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)
@@ -221,10 +222,7 @@
     }
 
     @Test
-    fun keyguardCallback_visibilityChanged_clockDozeCalled() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_visibilityChanged_clockDozeCalled() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
 
@@ -236,10 +234,7 @@
     }
 
     @Test
-    fun keyguardCallback_timeFormat_clockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onTimeFormatChanged("12h")
@@ -248,11 +243,8 @@
     }
 
     @Test
-    fun keyguardCallback_timezoneChanged_clockNotified() {
+    fun keyguardCallback_timezoneChanged_clockNotified() = runBlocking(IMMEDIATE) {
         val mockTimeZone = mock<TimeZone>()
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onTimeZoneChanged(mockTimeZone)
@@ -261,10 +253,7 @@
     }
 
     @Test
-    fun keyguardCallback_userSwitched_clockNotified() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
-
+    fun keyguardCallback_userSwitched_clockNotified() = runBlocking(IMMEDIATE) {
         val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
         verify(keyguardUpdateMonitor).registerCallback(capture(captor))
         captor.value.onUserSwitchComplete(10)
@@ -273,25 +262,27 @@
     }
 
     @Test
-    fun keyguardCallback_verifyKeyguardChanged() {
-        clockEventController.clock = clock
-        clockEventController.registerListeners()
+    fun keyguardCallback_verifyKeyguardChanged() = runBlocking(IMMEDIATE) {
+        val job = underTest.listenForDozeAmount(this)
+        repository.setDozeAmount(0.4f)
 
-        val captor = argumentCaptor<StatusBarStateController.StateListener>()
-        verify(statusBarStateController).addCallback(capture(captor))
-        captor.value.onDozeAmountChanged(0.4f, 0.6f)
+        yield()
 
         verify(animations).doze(0.4f)
+
+        job.cancel()
     }
 
     @Test
-    fun unregisterListeners_validate() {
-        clockEventController.clock = clock
-        clockEventController.unregisterListeners()
+    fun unregisterListeners_validate() = runBlocking(IMMEDIATE) {
+        underTest.unregisterListeners()
         verify(broadcastDispatcher).unregisterReceiver(any())
         verify(configurationController).removeCallback(any())
         verify(batteryController).removeCallback(any())
         verify(keyguardUpdateMonitor).removeCallback(any())
-        verify(statusBarStateController).removeCallback(any())
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 9b2bba6..61c7bb5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -44,7 +44,6 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
@@ -105,8 +104,6 @@
     private FrameLayout mLargeClockFrame;
     @Mock
     private SecureSettings mSecureSettings;
-    @Mock
-    private FeatureFlags mFeatureFlags;
 
     private final View mFakeSmartspaceView = new View(mContext);
 
@@ -143,8 +140,7 @@
                 mSecureSettings,
                 mExecutor,
                 mDumpManager,
-                mClockEventController,
-                mFeatureFlags
+                mClockEventController
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
@@ -280,6 +276,6 @@
     private void verifyAttachment(VerificationMode times) {
         verify(mClockRegistry, times).registerClockChangeListener(
                 any(ClockRegistry.ClockChangeListener.class));
-        verify(mClockEventController, times).registerListeners();
+        verify(mClockEventController, times).registerListeners(mView);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 48e8239..b885d54 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -146,6 +146,8 @@
 
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback;
+    @Captor
+    private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor;
 
     private Configuration mConfiguration;
 
@@ -475,6 +477,64 @@
         verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt());
     }
 
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardUpdateMonitor).requestFaceAuth(true,
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true);
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardUpdateMonitor, never())
+                .requestFaceAuth(true,
+                        FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.requestFaceAuth(true,
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(true);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardPasswordViewControllerMock).showMessage(null, null);
+    }
+
+    @Test
+    public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() {
+        KeyguardSecurityContainer.SwipeListener registeredSwipeListener =
+                getRegisteredSwipeListener();
+        when(mKeyguardUpdateMonitor.requestFaceAuth(true,
+                FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)).thenReturn(false);
+        setupGetSecurityView();
+
+        registeredSwipeListener.onSwipeUp();
+
+        verify(mKeyguardPasswordViewControllerMock, never()).showMessage(null, null);
+    }
+
+    private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() {
+        mKeyguardSecurityContainerController.onViewAttached();
+        verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture());
+        return mSwipeListenerArgumentCaptor.getValue();
+    }
+
     private void setupConditionsToEnableSideFpsHint() {
         attachView();
         setSideFpsHintEnabledFromResources(true);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 7281bc8..c6233b5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -26,6 +26,7 @@
 
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT;
+import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -648,6 +649,36 @@
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT);
     }
 
+    @Test
+    public void requestFaceAuth_whenFaceAuthWasStarted_returnsTrue() throws RemoteException {
+        // This satisfies all the preconditions to run face auth.
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+        bouncerFullyVisibleAndNotGoingToSleep();
+        mTestableLooper.processAllMessages();
+
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true,
+                NOTIFICATION_PANEL_CLICKED);
+
+        assertThat(didFaceAuthRun).isTrue();
+    }
+
+    @Test
+    public void requestFaceAuth_whenFaceAuthWasNotStarted_returnsFalse() throws RemoteException {
+        // This ensures face auth won't run.
+        biometricsDisabledForCurrentUser();
+        mTestableLooper.processAllMessages();
+
+        boolean didFaceAuthRun = mKeyguardUpdateMonitor.requestFaceAuth(true,
+                NOTIFICATION_PANEL_CLICKED);
+
+        assertThat(didFaceAuthRun).isFalse();
+    }
+
     private void testStrongAuthExceptOnBouncer(int strongAuth) {
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
new file mode 100644
index 0000000..dbf291c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.PointF;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MenuAnimationController}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class MenuAnimationControllerTest extends SysuiTestCase {
+    private MenuView mMenuView;
+    private MenuAnimationController mMenuAnimationController;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
+        mMenuAnimationController = new MenuAnimationController(mMenuView);
+    }
+
+    @Test
+    public void moveToPosition_matchPosition() {
+        final PointF destination = new PointF(50, 60);
+
+        mMenuAnimationController.moveToPosition(destination);
+
+        assertThat(mMenuView.getTranslationX()).isEqualTo(50);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(60);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
index d8b10e0..e62a329 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.accessibility.floatingmenu;
 
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.verify;
 
 import android.testing.AndroidTestingRunner;
@@ -25,6 +26,7 @@
 
 import com.android.systemui.SysuiTestCase;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,13 +44,24 @@
     @Mock
     private MenuInfoRepository.OnSettingsContentsChanged mMockSettingsContentsChanged;
 
+    private MenuInfoRepository mMenuInfoRepository;
+
+    @Before
+    public void setUp() {
+        mMenuInfoRepository = new MenuInfoRepository(mContext, mMockSettingsContentsChanged);
+    }
+
     @Test
     public void menuSizeTypeChanged_verifyOnSizeTypeChanged() {
-        final MenuInfoRepository menuInfoRepository =
-                new MenuInfoRepository(mContext,  mMockSettingsContentsChanged);
-
-        menuInfoRepository.mMenuSizeContentObserver.onChange(true);
+        mMenuInfoRepository.mMenuSizeContentObserver.onChange(true);
 
         verify(mMockSettingsContentsChanged).onSizeTypeChanged(anyInt());
     }
+
+    @Test
+    public void menuOpacityChanged_verifyOnFadeEffectChanged() {
+        mMenuInfoRepository.mMenuFadeOutContentObserver.onChange(true);
+
+        verify(mMockSettingsContentsChanged).onFadeEffectInfoChanged(any(MenuFadeEffectInfo.class));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
new file mode 100644
index 0000000..bf6d574
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Rect;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link MenuItemAccessibilityDelegate}. */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class MenuItemAccessibilityDelegateTest extends SysuiTestCase {
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    private RecyclerView mStubListView;
+    private MenuView mMenuView;
+    private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate;
+    private MenuAnimationController mMenuAnimationController;
+    private final Rect mDraggableBounds = new Rect(100, 200, 300, 400);
+
+    @Before
+    public void setUp() {
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                stubWindowManager);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+
+        final int halfScreenHeight =
+                stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2;
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        mMenuView.setTranslationY(halfScreenHeight);
+
+        doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds();
+        mStubListView = new RecyclerView(mContext);
+        mMenuAnimationController = spy(new MenuAnimationController(mMenuView));
+        mMenuItemAccessibilityDelegate =
+                new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate(
+                        mStubListView), mMenuAnimationController);
+    }
+
+    @Test
+    public void getAccessibilityActionList_matchSize() {
+        final AccessibilityNodeInfoCompat info =
+                new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo());
+
+        mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info);
+
+        assertThat(info.getActionList().size()).isEqualTo(5);
+    }
+
+    @Test
+    public void performMoveTopLeftAction_matchPosition() {
+        final boolean moveTopLeftAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_top_left,
+                        null);
+
+        assertThat(moveTopLeftAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
+    }
+
+    @Test
+    public void performMoveTopRightAction_matchPosition() {
+        final boolean moveTopRightAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_top_right, null);
+
+        assertThat(moveTopRightAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.top);
+    }
+
+    @Test
+    public void performMoveBottomLeftAction_matchPosition() {
+        final boolean moveBottomLeftAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_bottom_left, null);
+
+        assertThat(moveBottomLeftAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.left);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
+    }
+
+    @Test
+    public void performMoveBottomRightAction_matchPosition() {
+        final boolean moveBottomRightAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_bottom_right, null);
+
+        assertThat(moveBottomRightAction).isTrue();
+        assertThat(mMenuView.getTranslationX()).isEqualTo(mDraggableBounds.right);
+        assertThat(mMenuView.getTranslationY()).isEqualTo(mDraggableBounds.bottom);
+    }
+
+    @Test
+    public void performMoveToEdgeAndHideAction_success() {
+        final boolean moveToEdgeAndHideAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_to_edge_and_hide, null);
+
+        assertThat(moveToEdgeAndHideAction).isTrue();
+        verify(mMenuAnimationController).moveToEdgeAndHide();
+    }
+
+    @Test
+    public void performMoveOutFromEdgeAction_success() {
+        final boolean moveOutEdgeAndShowAction =
+                mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                        R.id.action_move_out_edge_and_show, null);
+
+        assertThat(moveOutEdgeAndShowAction).isTrue();
+        verify(mMenuAnimationController).moveOutEdgeAndShow();
+    }
+
+    @Test
+    public void performFocusAction_fadeIn() {
+        mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                ACTION_ACCESSIBILITY_FOCUS, null);
+
+        verify(mMenuAnimationController).fadeInNowIfEnabled();
+    }
+
+    @Test
+    public void performClearFocusAction_fadeOut() {
+        mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView,
+                ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
+
+        verify(mMenuAnimationController).fadeOutIfEnabled();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
new file mode 100644
index 0000000..c5b9a29
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.floatingmenu;
+
+import static android.view.View.OVER_SCROLL_NEVER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.WindowManager;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.accessibility.dialog.AccessibilityTarget;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.accessibility.MotionEventHelper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Tests for {@link MenuListViewTouchHandler}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class MenuListViewTouchHandlerTest extends SysuiTestCase {
+    private final List<AccessibilityTarget> mStubTargets = new ArrayList<>(
+            Collections.singletonList(mock(AccessibilityTarget.class)));
+    private final MotionEventHelper mMotionEventHelper = new MotionEventHelper();
+    private MenuView mStubMenuView;
+    private MenuListViewTouchHandler mTouchHandler;
+    private MenuAnimationController mMenuAnimationController;
+    private RecyclerView mStubListView;
+
+    @Before
+    public void setUp() throws Exception {
+        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
+        final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
+        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext,
+                windowManager);
+        mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance);
+        mStubMenuView.setTranslationX(0);
+        mStubMenuView.setTranslationY(0);
+        mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView));
+        mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController);
+        final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets);
+        mStubListView = (RecyclerView) mStubMenuView.getChildAt(0);
+        mStubListView.setAdapter(stubAdapter);
+    }
+
+    @Test
+    public void onActionDownEvent_shouldCancelAnimations() {
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+
+        verify(mMenuAnimationController).cancelAnimations();
+    }
+
+    @Test
+    public void onActionMoveEvent_shouldMoveToPosition() {
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mStubListView.setOverScrollMode(OVER_SCROLL_NEVER);
+
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+
+        assertThat(mStubMenuView.getTranslationX()).isEqualTo(offset);
+        assertThat(mStubMenuView.getTranslationY()).isEqualTo(offset);
+    }
+
+    @Test
+    public void dragAndDrop_shouldFlingMenuThenSpringToEdge() {
+        final int offset = 100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        final MotionEvent stubUpEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5,
+                        MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent);
+
+        verify(mMenuAnimationController).flingMenuThenSpringToEdge(anyFloat(), anyFloat(),
+                anyFloat());
+    }
+
+    @Test
+    public void dragMenuOutOfBoundsAndDrop_moveToLeftEdge_shouldMoveToEdgeAndHide() {
+        final int offset = -100;
+        final MotionEvent stubDownEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1,
+                        MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(),
+                        mStubMenuView.getTranslationY());
+        final MotionEvent stubMoveEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3,
+                        MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        final MotionEvent stubUpEvent =
+                mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 5,
+                        MotionEvent.ACTION_UP, mStubMenuView.getTranslationX() + offset,
+                        mStubMenuView.getTranslationY() + offset);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent);
+        mTouchHandler.onInterceptTouchEvent(mStubListView, stubUpEvent);
+
+        verify(mMenuAnimationController).moveToEdgeAndHide();
+    }
+
+    @After
+    public void tearDown() {
+        mMotionEventHelper.recycleEvents();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
index f782a44..8c8d6ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java
@@ -16,13 +16,24 @@
 
 package com.android.systemui.accessibility.floatingmenu;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
+import static android.view.WindowInsets.Type.displayCutout;
+import static android.view.WindowInsets.Type.systemBars;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.view.WindowMetrics;
 
 import androidx.test.filters.SmallTest;
 
@@ -38,6 +49,7 @@
 
 /** Tests for {@link MenuViewLayerController}. */
 @RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
 public class MenuViewLayerControllerTest extends SysuiTestCase {
     @Rule
@@ -46,10 +58,20 @@
     @Mock
     private WindowManager mWindowManager;
 
+    @Mock
+    private WindowMetrics mWindowMetrics;
+
     private MenuViewLayerController mMenuViewLayerController;
 
     @Before
     public void setUp() throws Exception {
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        doAnswer(invocation -> wm.getMaximumWindowMetrics()).when(
+                mWindowManager).getMaximumWindowMetrics();
+        mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
+        when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340));
+        when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets());
         mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager);
     }
 
@@ -68,4 +90,14 @@
 
         verify(mWindowManager).removeView(any(View.class));
     }
+
+    private WindowInsets stubDisplayInsets() {
+        final int stubStatusBarHeight = 118;
+        final int stubNavigationBarHeight = 125;
+        return new WindowInsets.Builder()
+                .setVisible(systemBars() | displayCutout(), true)
+                .setInsets(systemBars() | displayCutout(),
+                        Insets.of(0, stubStatusBarHeight, 0, stubNavigationBarHeight))
+                .build();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index 8883cb7..23c6ef1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -26,6 +26,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
@@ -44,7 +45,8 @@
 
     @Before
     public void setUp() throws Exception {
-        mMenuViewLayer = new MenuViewLayer(mContext);
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
index 513044d..742ee53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java
@@ -24,11 +24,15 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.UiModeManager;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.WindowManager;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.Prefs;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.After;
@@ -45,6 +49,8 @@
     private int mNightMode;
     private UiModeManager mUiModeManager;
     private MenuView mMenuView;
+    private String mLastPosition;
+    private MenuViewAppearance mStubMenuViewAppearance;
 
     @Before
     public void setUp() throws Exception {
@@ -52,8 +58,11 @@
         mNightMode = mUiModeManager.getNightMode();
         mUiModeManager.setNightMode(MODE_NIGHT_YES);
         final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext);
-        final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext);
-        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance));
+        final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class);
+        mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager);
+        mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance));
+        mLastPosition = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
     }
 
     @Test
@@ -74,8 +83,58 @@
         assertThat(areInsetsMatched).isTrue();
     }
 
+    @Test
+    public void onDraggingStart_matchInsets() {
+        mMenuView.onDraggingStart();
+        final InstantInsetLayerDrawable insetLayerDrawable =
+                (InstantInsetLayerDrawable) mMenuView.getBackground();
+
+        assertThat(insetLayerDrawable.getLayerInsetLeft(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetTop(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetRight(INDEX_MENU_ITEM)).isEqualTo(0);
+        assertThat(insetLayerDrawable.getLayerInsetBottom(INDEX_MENU_ITEM)).isEqualTo(0);
+    }
+
+    @Test
+    public void onAnimationend_updatePositionForSharedPreference() {
+        final float percentageX = 0.0f;
+        final float percentageY = 0.5f;
+
+        mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+        final String positionString = Prefs.getString(mContext,
+                Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null);
+        final Position position = Position.fromString(positionString);
+
+        assertThat(position.getPercentageX()).isEqualTo(percentageX);
+        assertThat(position.getPercentageY()).isEqualTo(percentageY);
+    }
+
+    @Test
+    public void onEdgeChangedIfNeeded_moveToLeftEdge_matchRadii() {
+        final Rect draggableBounds = mStubMenuViewAppearance.getMenuDraggableBounds();
+        mMenuView.setTranslationX(draggableBounds.right);
+
+        mMenuView.setTranslationX(draggableBounds.left);
+        mMenuView.onEdgeChangedIfNeeded();
+        final float[] radii = getMenuViewGradient().getCornerRadii();
+
+        assertThat(radii[0]).isEqualTo(0.0f);
+        assertThat(radii[1]).isEqualTo(0.0f);
+        assertThat(radii[6]).isEqualTo(0.0f);
+        assertThat(radii[7]).isEqualTo(0.0f);
+    }
+
+    private InstantInsetLayerDrawable getMenuViewInsetLayer() {
+        return (InstantInsetLayerDrawable) mMenuView.getBackground();
+    }
+
+    private GradientDrawable getMenuViewGradient() {
+        return (GradientDrawable) getMenuViewInsetLayer().getDrawable(INDEX_MENU_ITEM);
+    }
+
     @After
     public void tearDown() throws Exception {
         mUiModeManager.setNightMode(mNightMode);
+        Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, mLastPosition);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index f2ae7a1..d1107c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -54,6 +54,7 @@
 import org.mockito.Mockito.anyLong
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
@@ -125,6 +126,21 @@
     }
 
     @Test
+    fun testDismissBeforeIntroEnd() {
+        val container = initializeFingerprintContainer()
+        waitForIdleSync()
+
+        // STATE_ANIMATING_IN = 1
+        container?.mContainerState = 1
+
+        container.dismissWithoutCallback(false)
+
+        // the first time is triggered by initializeFingerprintContainer()
+        // the second time was triggered by dismissWithoutCallback()
+        verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+    }
+
+    @Test
     fun testDismissesOnFocusLoss() {
         val container = initializeFingerprintContainer()
         waitForIdleSync()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index cd50144..d489656 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -26,6 +26,7 @@
 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.LayoutInflater
@@ -124,14 +125,18 @@
         whenever(udfpsEnrollView.context).thenReturn(context)
     }
 
-    private fun withReason(@ShowReason reason: Int, block: () -> Unit) {
+    private fun withReason(
+        @ShowReason reason: Int,
+        isDebuggable: Boolean = false,
+        block: () -> Unit
+    ) {
         controllerOverlay = UdfpsControllerOverlay(
             context, fingerprintManager, inflater, windowManager, accessibilityManager,
             statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
             unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
-            controllerCallback, onTouch, activityLaunchAnimator
+            controllerCallback, onTouch, activityLaunchAnimator, isDebuggable
         )
         block()
     }
@@ -151,11 +156,29 @@
     }
 
     @Test
+    fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() {
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
+        withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) {
+            showUdfpsOverlay(isEnrollUseCase = false)
+        }
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
+    }
+
+    @Test
     fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) {
         showUdfpsOverlay(isEnrollUseCase = true)
     }
 
     @Test
+    fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() {
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
+        withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) {
+            showUdfpsOverlay(isEnrollUseCase = false)
+        }
+        Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
+    }
+
+    @Test
     fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() }
 
     private fun withRotation(@Rotation rotation: Int, block: () -> Unit) {
@@ -373,21 +396,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_0
         // touch at 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
         // touch at 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
         // touch at 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
     }
 
     fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
@@ -395,21 +430,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_90
         // touch at 0 degrees -> 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 90 degrees -> 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
         // touch at 180 degrees -> 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
         // touch at 270 degrees -> 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
     }
 
     fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
@@ -417,21 +464,33 @@
             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
         val rotation = Surface.ROTATION_270
         // touch at 0 degrees -> 270 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[3])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[3])
         // touch at 90 degrees -> 0 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[0])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[0])
         // touch at 180 degrees -> 90 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[1])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                -1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[1])
         // touch at 270 degrees -> 180 degrees
-        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
-                .isEqualTo(touchHints[2])
+        assertThat(
+            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
+                0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
+            )
+        ).isEqualTo(touchHints[2])
     }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index eff47bd..49c6fd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -665,7 +665,7 @@
         mUdfpsController.onAodInterrupt(0, 0, 0f, 0f);
         when(mUdfpsView.isDisplayConfigured()).thenReturn(true);
         // WHEN it is cancelled
-        mUdfpsController.onCancelUdfps();
+        mUdfpsController.cancelAodInterrupt();
         // THEN the display is unconfigured
         verify(mUdfpsView).unconfigureDisplay();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
index 3e9cf1e..fa9c41a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingCollectorImplTest.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerFake;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -71,6 +72,8 @@
     @Mock
     private KeyguardStateController mKeyguardStateController;
     @Mock
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
+    @Mock
     private BatteryController mBatteryController;
     private final DockManagerFake mDockManager = new DockManagerFake();
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -85,7 +88,8 @@
 
         mFalsingCollector = new FalsingCollectorImpl(mFalsingDataProvider, mFalsingManager,
                 mKeyguardUpdateMonitor, mHistoryTracker, mProximitySensor,
-                mStatusBarStateController, mKeyguardStateController, mBatteryController,
+                mStatusBarStateController, mKeyguardStateController, mShadeExpansionStateManager,
+                mBatteryController,
                 mDockManager, mFakeExecutor, mFakeSystemClock);
     }
 
@@ -137,9 +141,9 @@
     public void testUnregisterSensor_QS() {
         mFalsingCollector.onScreenTurningOn();
         reset(mProximitySensor);
-        mFalsingCollector.setQsExpanded(true);
+        mFalsingCollector.onQsExpansionChanged(true);
         verify(mProximitySensor).unregister(any(ThresholdSensor.Listener.class));
-        mFalsingCollector.setQsExpanded(false);
+        mFalsingCollector.onQsExpansionChanged(false);
         verify(mProximitySensor).register(any(ThresholdSensor.Listener.class));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index d96ca91..b7f1c1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -39,6 +39,7 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.screenshot.TimeoutHandler;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -95,6 +96,11 @@
         mCallbacks = mOverlayCallbacksCaptor.getValue();
     }
 
+    @After
+    public void tearDown() {
+        mOverlayController.hideImmediate();
+    }
+
     @Test
     public void test_setClipData_nullData() {
         ClipData clipData = null;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
index 6a55a60..5bbd810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeMachineTest.java
@@ -16,6 +16,9 @@
 
 package com.android.systemui.doze;
 
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_DOCKED;
@@ -38,16 +41,17 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.UiModeManager;
 import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
 import android.view.Display;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -78,25 +82,30 @@
     @Mock
     private DozeHost mHost;
     @Mock
-    private UiModeManager mUiModeManager;
+    private DozeMachine.Part mPartMock;
+    @Mock
+    private DozeMachine.Part mAnotherPartMock;
     private DozeServiceFake mServiceFake;
     private WakeLockFake mWakeLockFake;
-    private AmbientDisplayConfiguration mConfigMock;
-    private DozeMachine.Part mPartMock;
+    private AmbientDisplayConfiguration mAmbientDisplayConfigMock;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mServiceFake = new DozeServiceFake();
         mWakeLockFake = new WakeLockFake();
-        mConfigMock = mock(AmbientDisplayConfiguration.class);
-        mPartMock = mock(DozeMachine.Part.class);
+        mAmbientDisplayConfigMock = mock(AmbientDisplayConfiguration.class);
         when(mDockManager.isDocked()).thenReturn(false);
         when(mDockManager.isHidden()).thenReturn(false);
 
-        mMachine = new DozeMachine(mServiceFake, mConfigMock, mWakeLockFake,
-                mWakefulnessLifecycle, mUiModeManager, mDozeLog, mDockManager,
-                mHost, new DozeMachine.Part[]{mPartMock});
+        mMachine = new DozeMachine(mServiceFake,
+                mAmbientDisplayConfigMock,
+                mWakeLockFake,
+                mWakefulnessLifecycle,
+                mDozeLog,
+                mDockManager,
+                mHost,
+                new DozeMachine.Part[]{mPartMock, mAnotherPartMock});
     }
 
     @Test
@@ -108,7 +117,7 @@
 
     @Test
     public void testInitialize_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
 
         mMachine.requestState(INITIALIZED);
 
@@ -118,7 +127,7 @@
 
     @Test
     public void testInitialize_goesToAod() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
 
         mMachine.requestState(INITIALIZED);
 
@@ -138,7 +147,7 @@
 
     @Test
     public void testInitialize_afterDockPaused_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -151,7 +160,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
 
         mMachine.requestState(INITIALIZED);
 
@@ -162,7 +171,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
 
         mMachine.requestState(INITIALIZED);
 
@@ -184,7 +193,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnDisabled_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -197,7 +206,7 @@
     @Test
     public void testInitialize_alwaysOnSuppressed_alwaysOnEnabled_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
 
@@ -209,7 +218,7 @@
 
     @Test
     public void testPulseDone_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(false);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -222,7 +231,7 @@
 
     @Test
     public void testPulseDone_goesToAoD() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -236,7 +245,7 @@
     @Test
     public void testPulseDone_alwaysOnSuppressed_goesToSuppressed() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         mMachine.requestState(INITIALIZED);
         mMachine.requestPulse(DozeLog.PULSE_REASON_NOTIFICATION);
         mMachine.requestState(DOZE_PULSING);
@@ -287,7 +296,7 @@
 
     @Test
     public void testPulseDone_afterDockPaused_goesToDoze() {
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
         mMachine.requestState(INITIALIZED);
@@ -303,7 +312,7 @@
     @Test
     public void testPulseDone_alwaysOnSuppressed_afterDockPaused_goesToDoze() {
         when(mHost.isAlwaysOnSuppressed()).thenReturn(true);
-        when(mConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
+        when(mAmbientDisplayConfigMock.alwaysOnEnabled(anyInt())).thenReturn(true);
         when(mDockManager.isDocked()).thenReturn(true);
         when(mDockManager.isHidden()).thenReturn(true);
         mMachine.requestState(INITIALIZED);
@@ -471,7 +480,9 @@
 
     @Test
     public void testTransitionToInitialized_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
 
         verify(mPartMock).transitionTo(UNINITIALIZED, INITIALIZED);
@@ -481,7 +492,9 @@
 
     @Test
     public void testTransitionToFinish_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(FINISH);
 
@@ -490,7 +503,9 @@
 
     @Test
     public void testDozeToDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE);
 
@@ -499,7 +514,9 @@
 
     @Test
     public void testDozeAoDToDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_AOD);
 
@@ -508,7 +525,9 @@
 
     @Test
     public void testDozePulsingBrightDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_PULSING_BRIGHT);
 
@@ -517,7 +536,9 @@
 
     @Test
     public void testDozeAodDockedDozeSuspendTriggers_carModeIsEnabled() {
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        Configuration configuration = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(configuration);
         mMachine.requestState(INITIALIZED);
         mMachine.requestState(DOZE_AOD_DOCKED);
 
@@ -525,7 +546,35 @@
     }
 
     @Test
+    public void testOnConfigurationChanged_propagatesUiModeTypeToParts() {
+        Configuration newConfig = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(newConfig);
+
+        verify(mPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mAnotherPartMock).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+    }
+
+    @Test
+    public void testOnConfigurationChanged_propagatesOnlyUiModeChangesToParts() {
+        Configuration newConfig = configWithCarNightUiMode();
+
+        mMachine.onConfigurationChanged(newConfig);
+        mMachine.onConfigurationChanged(newConfig);
+
+        verify(mPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mAnotherPartMock, times(1)).onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+    }
+
+    @Test
     public void testDozeSuppressTriggers_screenState() {
         assertEquals(Display.STATE_OFF, DOZE_SUSPEND_TRIGGERS.screenState(null));
     }
+
+    @NonNull
+    private Configuration configWithCarNightUiMode() {
+        Configuration configuration = Configuration.EMPTY;
+        configuration.uiMode = UI_MODE_TYPE_CAR | UI_MODE_NIGHT_YES;
+        return configuration;
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index b33f9a7..2f206ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -425,7 +425,7 @@
 
     @Test
     public void testGesturesAllInitiallyRespectSettings() {
-        DozeSensors dozeSensors = new DozeSensors(getContext(), mSensorManager, mDozeParameters,
+        DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters,
                 mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
                 mProximitySensor, mFakeSettings, mAuthController,
                 mDevicePostureController);
@@ -437,7 +437,7 @@
 
     private class TestableDozeSensors extends DozeSensors {
         TestableDozeSensors() {
-            super(getContext(), mSensorManager, mDozeParameters,
+            super(mSensorManager, mDozeParameters,
                     mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
                     mProximitySensor, mFakeSettings, mAuthController,
                     mDevicePostureController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
index 0f29dcd..32b9945 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSuppressorTest.java
@@ -10,14 +10,14 @@
  * 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 andatest
+ * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 package com.android.systemui.doze;
 
-import static android.app.UiModeManager.ACTION_ENTER_CAR_MODE;
-import static android.app.UiModeManager.ACTION_EXIT_CAR_MODE;
+import static android.content.res.Configuration.UI_MODE_TYPE_CAR;
+import static android.content.res.Configuration.UI_MODE_TYPE_NORMAL;
 
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
@@ -26,17 +26,16 @@
 import static com.android.systemui.doze.DozeMachine.State.INITIALIZED;
 import static com.android.systemui.doze.DozeMachine.State.UNINITIALIZED;
 
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.UiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.testing.AndroidTestingRunner;
 import android.testing.UiThreadTest;
@@ -44,13 +43,13 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -71,10 +70,6 @@
     @Mock
     private AmbientDisplayConfiguration mConfig;
     @Mock
-    private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
-    private UiModeManager mUiModeManager;
-    @Mock
     private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     @Mock
     private BiometricUnlockController mBiometricUnlockController;
@@ -83,13 +78,6 @@
     private DozeMachine mDozeMachine;
 
     @Captor
-    private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
-    @Captor
-    private ArgumentCaptor<IntentFilter> mIntentFilterCaptor;
-    private BroadcastReceiver mBroadcastReceiver;
-    private IntentFilter mIntentFilter;
-
-    @Captor
     private ArgumentCaptor<DozeHost.Callback> mDozeHostCaptor;
     private DozeHost.Callback mDozeHostCallback;
 
@@ -106,8 +94,6 @@
                 mDozeHost,
                 mConfig,
                 mDozeLog,
-                mBroadcastDispatcher,
-                mUiModeManager,
                 mBiometricUnlockControllerLazy);
 
         mDozeSuppressor.setDozeMachine(mDozeMachine);
@@ -122,36 +108,35 @@
     public void testRegistersListenersOnInitialized_unregisteredOnFinish() {
         // check that receivers and callbacks registered
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         captureDozeHostCallback();
 
         // check that receivers and callbacks are unregistered
         mDozeSuppressor.transitionTo(INITIALIZED, FINISH);
-        verify(mBroadcastDispatcher).unregisterReceiver(mBroadcastReceiver);
         verify(mDozeHost).removeCallback(mDozeHostCallback);
     }
 
     @Test
     public void testSuspendTriggersDoze_carMode() {
         // GIVEN car mode
-        when(mUiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
 
         // WHEN dozing begins
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
 
         // THEN doze continues with all doze triggers disabled.
-        verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, atLeastOnce()).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, never())
+                .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
     }
 
     @Test
     public void testSuspendTriggersDoze_enterCarMode() {
         // GIVEN currently dozing
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE);
 
         // WHEN car mode entered
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_ENTER_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
 
         // THEN doze continues with all doze triggers disabled.
         verify(mDozeMachine).requestState(DOZE_SUSPEND_TRIGGERS);
@@ -160,13 +145,13 @@
     @Test
     public void testDozeResume_exitCarMode() {
         // GIVEN currently suspended, with AOD not enabled
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
         when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(false);
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
 
         // WHEN exiting car mode
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
 
         // THEN doze is resumed
         verify(mDozeMachine).requestState(DOZE);
@@ -175,19 +160,53 @@
     @Test
     public void testDozeAoDResume_exitCarMode() {
         // GIVEN currently suspended, with AOD not enabled
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
         when(mConfig.alwaysOnEnabled(anyInt())).thenReturn(true);
         mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
-        captureBroadcastReceiver();
         mDozeSuppressor.transitionTo(INITIALIZED, DOZE_SUSPEND_TRIGGERS);
 
         // WHEN exiting car mode
-        mBroadcastReceiver.onReceive(null, new Intent(ACTION_EXIT_CAR_MODE));
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_NORMAL);
 
         // THEN doze AOD is resumed
         verify(mDozeMachine).requestState(DOZE_AOD);
     }
 
     @Test
+    public void testUiModeDoesNotChange_noStateTransition() {
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+        clearInvocations(mDozeMachine);
+
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+        verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+        verify(mDozeMachine, never())
+                .requestState(AdditionalMatchers.not(eq(DOZE_SUSPEND_TRIGGERS)));
+    }
+
+    @Test
+    public void testUiModeTypeChange_whenDozeMachineIsNotReady_doesNotDoAnything() {
+        when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+
+        verify(mDozeMachine, never()).requestState(any());
+    }
+
+    @Test
+    public void testUiModeTypeChange_CarModeEnabledAndDozeMachineNotReady_suspendsTriggersAfter() {
+        when(mDozeMachine.isUninitializedOrFinished()).thenReturn(true);
+        mDozeSuppressor.onUiModeTypeChanged(UI_MODE_TYPE_CAR);
+        verify(mDozeMachine, never()).requestState(any());
+
+        mDozeSuppressor.transitionTo(UNINITIALIZED, INITIALIZED);
+
+        verify(mDozeMachine, times(1)).requestState(DOZE_SUSPEND_TRIGGERS);
+    }
+
+    @Test
     public void testEndDoze_unprovisioned() {
         // GIVEN device unprovisioned
         when(mDozeHost.isProvisioned()).thenReturn(false);
@@ -276,14 +295,4 @@
         verify(mDozeHost).addCallback(mDozeHostCaptor.capture());
         mDozeHostCallback = mDozeHostCaptor.getValue();
     }
-
-    private void captureBroadcastReceiver() {
-        verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiverCaptor.capture(),
-                mIntentFilterCaptor.capture());
-        mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
-        mIntentFilter = mIntentFilterCaptor.getValue();
-        assertEquals(2, mIntentFilter.countActions());
-        org.hamcrest.MatcherAssert.assertThat(() -> mIntentFilter.actionsIterator(),
-                containsInAnyOrder(ACTION_ENTER_CAR_MODE, ACTION_EXIT_CAR_MODE));
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 781dc15..6091d3a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -23,10 +23,10 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -88,6 +88,8 @@
     @Mock
     private ProximityCheck mProximityCheck;
     @Mock
+    private DozeLog mDozeLog;
+    @Mock
     private AuthController mAuthController;
     @Mock
     private UiEventLogger mUiEventLogger;
@@ -127,7 +129,7 @@
 
         mTriggers = new DozeTriggers(mContext, mHost, config, dozeParameters,
                 asyncSensorManager, wakeLock, mDockManager, mProximitySensor,
-                mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(),
+                mProximityCheck, mDozeLog, mBroadcastDispatcher, new FakeSettings(),
                 mAuthController, mUiEventLogger, mSessionTracker, mKeyguardStateController,
                 mDevicePostureController);
         mTriggers.setDozeMachine(mMachine);
@@ -342,6 +344,16 @@
         verify(mProximityCheck).destroy();
     }
 
+    @Test
+    public void testIsExecutingTransition_dropPulse() {
+        when(mHost.isPulsePending()).thenReturn(false);
+        when(mMachine.isExecutingTransition()).thenReturn(true);
+
+        mTriggers.onSensor(DozeLog.PULSE_REASON_SENSOR_LONG_PRESS, 100, 100, null);
+
+        verify(mDozeLog).tracePulseDropped(anyString(), eq(null));
+    }
+
     private void waitForSensorManager() {
         mExecutor.runAllReady();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
index 522b5b5..50f27ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
@@ -33,7 +33,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaCarouselController;
+import com.android.systemui.media.controls.ui.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
index 65b44a1..65ae90b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -19,11 +19,17 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.CoreStartable
 import com.android.systemui.Dumpable
+import com.android.systemui.ProtoDumpable
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.system.UncaughtExceptionPreHandlerManager
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.google.common.truth.Truth.assertThat
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.io.StringWriter
+import javax.inject.Provider
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -31,9 +37,6 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import java.io.PrintWriter
-import java.io.StringWriter
-import javax.inject.Provider
 
 @SmallTest
 class DumpHandlerTest : SysuiTestCase() {
@@ -47,6 +50,8 @@
 
     @Mock
     private lateinit var pw: PrintWriter
+    @Mock
+    private lateinit var fd: FileDescriptor
 
     @Mock
     private lateinit var dumpable1: Dumpable
@@ -56,6 +61,11 @@
     private lateinit var dumpable3: Dumpable
 
     @Mock
+    private lateinit var protoDumpable1: ProtoDumpable
+    @Mock
+    private lateinit var protoDumpable2: ProtoDumpable
+
+    @Mock
     private lateinit var buffer1: LogBuffer
     @Mock
     private lateinit var buffer2: LogBuffer
@@ -88,7 +98,7 @@
 
         // WHEN some of them are dumped explicitly
         val args = arrayOf("dumpable1", "dumpable3", "buffer2")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN only the requested ones have their dump() method called
         verify(dumpable1).dump(pw, args)
@@ -107,7 +117,7 @@
 
         // WHEN that module is dumped
         val args = arrayOf("dumpable1")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN its dump() method is called
         verify(dumpable1).dump(pw, args)
@@ -124,7 +134,7 @@
 
         // WHEN a critical dump is requested
         val args = arrayOf("--dump-priority", "CRITICAL")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN all modules are dumped (but no buffers)
         verify(dumpable1).dump(pw, args)
@@ -145,7 +155,7 @@
 
         // WHEN a normal dump is requested
         val args = arrayOf("--dump-priority", "NORMAL")
-        dumpHandler.dump(pw, args)
+        dumpHandler.dump(fd, pw, args)
 
         // THEN all buffers are dumped (but no modules)
         verify(dumpable1, never()).dump(
@@ -168,11 +178,35 @@
         val spw = PrintWriter(stringWriter)
 
         // When a config dump is requested
-        dumpHandler.dump(spw, arrayOf("config"))
+        dumpHandler.dump(fd, spw, arrayOf("config"))
 
         assertThat(stringWriter.toString()).contains(EmptyCoreStartable::class.java.simpleName)
     }
 
+    @Test
+    fun testDumpAllProtoDumpables() {
+        dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+        dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+        val args = arrayOf(DumpHandler.PROTO)
+        dumpHandler.dump(fd, pw, args)
+
+        verify(protoDumpable1).dumpProto(any(), eq(args))
+        verify(protoDumpable2).dumpProto(any(), eq(args))
+    }
+
+    @Test
+    fun testDumpSingleProtoDumpable() {
+        dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+        dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
+
+        val args = arrayOf(DumpHandler.PROTO, "protoDumpable1")
+        dumpHandler.dump(fd, pw, args)
+
+        verify(protoDumpable1).dumpProto(any(), eq(args))
+        verify(protoDumpable2, never()).dumpProto(any(), any())
+    }
+
     private class EmptyCoreStartable : CoreStartable {
         override fun start() {}
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
index 4c61138..9628ee9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagCommandTest.kt
@@ -51,14 +51,6 @@
     }
 
     @Test
-    fun noOpCommand() {
-        cmd.execute(pw, ArrayList())
-        Mockito.verify(pw, Mockito.atLeastOnce()).println()
-        Mockito.verify(featureFlags).isEnabled(flagA)
-        Mockito.verify(featureFlags).isEnabled(flagB)
-    }
-
-    @Test
     fun readFlagCommand() {
         cmd.execute(pw, listOf(flagA.id.toString()))
         Mockito.verify(featureFlags).isEnabled(flagA)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
new file mode 100644
index 0000000..1b34100
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import android.animation.AnimationHandler.AnimationFrameCallbackProvider
+import android.animation.ValueAnimator
+import android.util.Log
+import android.util.Log.TerribleFailure
+import android.util.Log.TerribleFailureHandler
+import android.view.Choreographer.FrameCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.google.common.truth.Truth.assertThat
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.UUID
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardTransitionRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardTransitionRepository
+    private lateinit var oldWtfHandler: TerribleFailureHandler
+    private lateinit var wtfHandler: WtfHandler
+
+    @Before
+    fun setUp() {
+        underTest = KeyguardTransitionRepository()
+        wtfHandler = WtfHandler()
+        oldWtfHandler = Log.setWtfHandler(wtfHandler)
+    }
+
+    @After
+    fun tearDown() {
+        oldWtfHandler?.let { Log.setWtfHandler(it) }
+    }
+
+    @Test
+    fun `startTransition runs animator to completion`() =
+        runBlocking(IMMEDIATE) {
+            val (animator, provider) = setupAnimator(this)
+
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator))
+
+            val startTime = System.currentTimeMillis()
+            while (animator.isRunning()) {
+                yield()
+                if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) {
+                    fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION")
+                }
+            }
+
+            assertSteps(steps, listWithStep(BigDecimal(.1)))
+
+            job.cancel()
+            provider.stop()
+        }
+
+    @Test
+    fun `startTransition called during another transition fails`() {
+        underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null))
+        underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null))
+
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    @Test
+    fun `Null animator enables manual control with updateTransition`() =
+        runBlocking(IMMEDIATE) {
+            val steps = mutableListOf<TransitionStep>()
+            val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this)
+
+            val uuid =
+                underTest.startTransition(
+                    TransitionInfo(
+                        ownerName = OWNER_NAME,
+                        from = AOD,
+                        to = LOCKSCREEN,
+                        animator = null,
+                    )
+                )
+
+            checkNotNull(uuid).let {
+                underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+                underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+            }
+
+            assertThat(steps.size).isEqualTo(3)
+            assertThat(steps[0])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+            assertThat(steps[1])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING))
+            assertThat(steps[2])
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+            job.cancel()
+        }
+
+    @Test
+    fun `Attempt to  manually update transition with invalid UUID throws exception`() {
+        underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    @Test
+    fun `Attempt to manually update transition after FINISHED state throws exception`() {
+        val uuid =
+            underTest.startTransition(
+                TransitionInfo(
+                    ownerName = OWNER_NAME,
+                    from = AOD,
+                    to = LOCKSCREEN,
+                    animator = null,
+                )
+            )
+
+        checkNotNull(uuid).let {
+            underTest.updateTransition(it, 1f, TransitionState.FINISHED)
+            underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
+        }
+        assertThat(wtfHandler.failed).isTrue()
+    }
+
+    private fun listWithStep(step: BigDecimal): List<BigDecimal> {
+        val steps = mutableListOf<BigDecimal>()
+
+        var i = BigDecimal.ZERO
+        while (i.compareTo(BigDecimal.ONE) <= 0) {
+            steps.add(i)
+            i = (i + step).setScale(2, RoundingMode.HALF_UP)
+        }
+
+        return steps
+    }
+
+    private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) {
+        // + 2 accounts for start and finish of automated transition
+        assertThat(steps.size).isEqualTo(fractions.size + 2)
+
+        assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+        fractions.forEachIndexed { index, fraction ->
+            assertThat(steps[index + 1])
+                .isEqualTo(
+                    TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING)
+                )
+        }
+        assertThat(steps[steps.size - 1])
+            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+
+        assertThat(wtfHandler.failed).isFalse()
+    }
+
+    private fun setupAnimator(
+        scope: CoroutineScope
+    ): Pair<ValueAnimator, TestFrameCallbackProvider> {
+        val animator =
+            ValueAnimator().apply {
+                setInterpolator(Interpolators.LINEAR)
+                setDuration(ANIMATION_DURATION)
+            }
+
+        val provider = TestFrameCallbackProvider(animator, scope)
+        provider.start()
+
+        return Pair(animator, provider)
+    }
+
+    /** Gives direct control over ValueAnimator. See [AnimationHandler] */
+    private class TestFrameCallbackProvider(
+        private val animator: ValueAnimator,
+        private val scope: CoroutineScope,
+    ) : AnimationFrameCallbackProvider {
+
+        private var frameCount = 1L
+        private var frames = MutableStateFlow(Pair<Long, FrameCallback?>(0L, null))
+        private var job: Job? = null
+
+        fun start() {
+            animator.getAnimationHandler().setProvider(this)
+
+            job =
+                scope.launch {
+                    frames.collect {
+                        // Delay is required for AnimationHandler to properly register a callback
+                        delay(1)
+                        val (frameNumber, callback) = it
+                        callback?.doFrame(frameNumber)
+                    }
+                }
+        }
+
+        fun stop() {
+            job?.cancel()
+            animator.getAnimationHandler().setProvider(null)
+        }
+
+        override fun postFrameCallback(cb: FrameCallback) {
+            frames.value = Pair(++frameCount, cb)
+        }
+        override fun postCommitCallback(runnable: Runnable) {}
+        override fun getFrameTime() = frameCount
+        override fun getFrameDelay() = 1L
+        override fun setFrameDelay(delay: Long) {}
+    }
+
+    private class WtfHandler : TerribleFailureHandler {
+        var failed = false
+        override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) {
+            failed = true
+        }
+    }
+
+    companion object {
+        private const val MAX_TEST_DURATION = 100L
+        private const val ANIMATION_DURATION = 10L
+        private const val OWNER_NAME = "Test"
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
deleted file mode 100644
index 7e0be6d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ /dev/null
@@ -1,476 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.app.PendingIntent
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import androidx.test.filters.SmallTest
-import com.android.internal.logging.InstanceId
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.PAGINATION_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
-import com.android.systemui.media.MediaHierarchyManager.Companion.LOCATION_QS
-import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.time.FakeSystemClock
-import javax.inject.Provider
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-import org.mockito.MockitoAnnotations
-
-private val DATA = MediaTestUtils.emptyMediaData
-
-private val SMARTSPACE_KEY = "smartspace"
-
-@SmallTest
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
-class MediaCarouselControllerTest : SysuiTestCase() {
-
-    @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
-    @Mock lateinit var panel: MediaControlPanel
-    @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
-    @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
-    @Mock lateinit var mediaHostState: MediaHostState
-    @Mock lateinit var activityStarter: ActivityStarter
-    @Mock @Main private lateinit var executor: DelayableExecutor
-    @Mock lateinit var mediaDataManager: MediaDataManager
-    @Mock lateinit var configurationController: ConfigurationController
-    @Mock lateinit var falsingCollector: FalsingCollector
-    @Mock lateinit var falsingManager: FalsingManager
-    @Mock lateinit var dumpManager: DumpManager
-    @Mock lateinit var logger: MediaUiEventLogger
-    @Mock lateinit var debugLogger: MediaCarouselControllerLogger
-    @Mock lateinit var mediaViewController: MediaViewController
-    @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
-    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
-    @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
-
-    private val clock = FakeSystemClock()
-    private lateinit var mediaCarouselController: MediaCarouselController
-
-    @Before
-    fun setup() {
-        MockitoAnnotations.initMocks(this)
-        mediaCarouselController = MediaCarouselController(
-            context,
-            mediaControlPanelFactory,
-            visualStabilityProvider,
-            mediaHostStatesManager,
-            activityStarter,
-            clock,
-            executor,
-            mediaDataManager,
-            configurationController,
-            falsingCollector,
-            falsingManager,
-            dumpManager,
-            logger,
-            debugLogger
-        )
-        verify(mediaDataManager).addListener(capture(listener))
-        verify(visualStabilityProvider)
-            .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
-        whenever(mediaControlPanelFactory.get()).thenReturn(panel)
-        whenever(panel.mediaViewController).thenReturn(mediaViewController)
-        whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
-        MediaPlayerData.clear()
-    }
-
-    @Test
-    fun testPlayerOrdering() {
-        // Test values: key, data, last active time
-        val playingLocal = Triple("playing local",
-            DATA.copy(active = true, isPlaying = true,
-                    playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
-            4500L)
-
-        val playingCast = Triple("playing cast",
-            DATA.copy(active = true, isPlaying = true,
-                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
-            5000L)
-
-        val pausedLocal = Triple("paused local",
-            DATA.copy(active = true, isPlaying = false,
-                    playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false),
-            1000L)
-
-        val pausedCast = Triple("paused cast",
-            DATA.copy(active = true, isPlaying = false,
-                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL, resumption = false),
-            2000L)
-
-        val playingRcn = Triple("playing RCN",
-            DATA.copy(active = true, isPlaying = true,
-                    playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
-            5000L)
-
-        val pausedRcn = Triple("paused RCN",
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_CAST_REMOTE, resumption = false),
-                5000L)
-
-        val active = Triple("active",
-            DATA.copy(active = true, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
-            250L)
-
-        val resume1 = Triple("resume 1",
-            DATA.copy(active = false, isPlaying = false,
-                    playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
-            500L)
-
-        val resume2 = Triple("resume 2",
-            DATA.copy(active = false, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true),
-            1000L)
-
-        val activeMoreRecent = Triple("active more recent",
-            DATA.copy(active = false, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 2L),
-            1000L)
-
-        val activeLessRecent = Triple("active less recent",
-            DATA.copy(active = false, isPlaying = false,
-                playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true, lastActive = 1L),
-            1000L)
-        // Expected ordering for media players:
-        // Actively playing local sessions
-        // Actively playing cast sessions
-        // Paused local and cast sessions, by last active
-        // RCNs
-        // Resume controls, by last active
-
-        val expected = listOf(playingLocal, playingCast, pausedCast, pausedLocal, playingRcn,
-                pausedRcn, active, resume2, resume1)
-
-        expected.forEach {
-            clock.setCurrentTimeMillis(it.third)
-            MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first),
-                panel, clock, isSsReactivated = false)
-        }
-
-        for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
-            assertEquals(expected.get(index).first, key.data.notificationKey)
-        }
-
-        for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) {
-            assertEquals(expected.get(index).first, key.data.notificationKey)
-        }
-    }
-
-    @Test
-    fun testOrderWithSmartspace_prioritized() {
-        testPlayerOrdering()
-
-        // If smartspace is prioritized
-        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
-            true, clock)
-
-        // Then it should be shown immediately after any actively playing controls
-        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
-    }
-
-    @Test
-    fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() {
-        testPlayerOrdering()
-
-        // If smartspace is prioritized
-        listener.value.onSmartspaceMediaDataLoaded(
-                SMARTSPACE_KEY,
-                EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
-                true
-        )
-
-        // Then it should be shown immediately after any actively playing controls
-        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
-        assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec)
-    }
-
-    @Test
-    fun testOrderWithSmartspace_notPrioritized() {
-        testPlayerOrdering()
-
-        // If smartspace is not prioritized
-        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
-            false, clock)
-
-        // Then it should be shown at the end of the carousel's active entries
-        val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
-        assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
-    }
-
-    @Test
-    fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
-        testPlayerOrdering()
-        // playing paused player
-        listener.value.onMediaDataLoaded("paused local",
-                "paused local",
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-        listener.value.onMediaDataLoaded("playing local",
-                "playing local",
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true)
-        )
-
-        assertEquals(
-                MediaPlayerData.getMediaPlayerIndex("paused local"),
-                mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
-        )
-        // paused player order should stays the same in visibleMediaPLayer map.
-        // paused player order should be first in mediaPlayer map.
-        assertEquals(
-                MediaPlayerData.visiblePlayerKeys().elementAt(3),
-                MediaPlayerData.playerKeys().elementAt(0)
-        )
-    }
-    @Test
-    fun testSwipeDismiss_logged() {
-        mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
-
-        verify(logger).logSwipeDismiss()
-    }
-
-    @Test
-    fun testSettingsButton_logged() {
-        mediaCarouselController.settingsButton.callOnClick()
-
-        verify(logger).logCarouselSettings()
-    }
-
-    @Test
-    fun testLocationChangeQs_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_QS,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
-    }
-
-    @Test
-    fun testLocationChangeQqs_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_QQS,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
-    }
-
-    @Test
-    fun testLocationChangeLockscreen_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_LOCKSCREEN,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
-    }
-
-    @Test
-    fun testLocationChangeDream_logged() {
-        mediaCarouselController.onDesiredLocationChanged(
-            MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
-            mediaHostState,
-            animate = false)
-        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
-    }
-
-    @Test
-    fun testRecommendationRemoved_logged() {
-        val packageName = "smartspace package"
-        val instanceId = InstanceId.fakeInstanceId(123)
-
-        val smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-            packageName = packageName,
-            instanceId = instanceId
-        )
-        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
-        mediaCarouselController.removePlayer(SMARTSPACE_KEY)
-
-        verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
-    }
-
-    @Test
-    fun testMediaLoaded_ScrollToActivePlayer() {
-        listener.value.onMediaDataLoaded("playing local",
-                null,
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
-        )
-        listener.value.onMediaDataLoaded("paused local",
-                null,
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-        // adding a media recommendation card.
-        listener.value.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA,
-                false)
-        mediaCarouselController.shouldScrollToKey = true
-        // switching between media players.
-        listener.value.onMediaDataLoaded("playing local",
-        "playing local",
-                DATA.copy(active = true, isPlaying = false,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = true)
-        )
-        listener.value.onMediaDataLoaded("paused local",
-                "paused local",
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false))
-
-        assertEquals(
-                MediaPlayerData.getMediaPlayerIndex("paused local"),
-                mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
-        )
-    }
-
-    @Test
-    fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
-        listener.value.onSmartspaceMediaDataLoaded(
-                SMARTSPACE_KEY,
-                EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
-                false
-        )
-        listener.value.onMediaDataLoaded("playing local",
-                null,
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false)
-        )
-
-        var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
-        assertEquals(
-                playerIndex,
-                mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
-        )
-        assertEquals(playerIndex, 0)
-
-        // Replaying the same media player one more time.
-        // And check that the card stays in its position.
-        mediaCarouselController.shouldScrollToKey = true
-        listener.value.onMediaDataLoaded("playing local",
-                null,
-                DATA.copy(active = true, isPlaying = true,
-                        playbackLocation = MediaData.PLAYBACK_LOCAL, resumption = false,
-                        packageName = "PACKAGE_NAME")
-        )
-        playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
-        assertEquals(playerIndex, 0)
-    }
-
-    @Test
-    fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
-        var result = false
-        mediaCarouselController.updateHostVisibility = { result = true }
-
-        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
-        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
-
-        assertEquals(true, result)
-    }
-
-    @Test
-    fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
-        var result = false
-        mediaCarouselController.updateHostVisibility = { result = true }
-
-        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
-        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
-        assertEquals(false, result)
-
-        visualStabilityCallback.value.onReorderingAllowed()
-        assertEquals(true, result)
-    }
-
-    @Test
-    fun testGetCurrentVisibleMediaContentIntent() {
-        val clickIntent1 = mock(PendingIntent::class.java)
-        val player1 = Triple("player1",
-                DATA.copy(clickIntent = clickIntent1),
-                1000L)
-        clock.setCurrentTimeMillis(player1.third)
-        MediaPlayerData.addMediaPlayer(player1.first,
-                player1.second.copy(notificationKey = player1.first),
-                panel, clock, isSsReactivated = false)
-
-        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
-
-        val clickIntent2 = mock(PendingIntent::class.java)
-        val player2 = Triple("player2",
-                DATA.copy(clickIntent = clickIntent2),
-                2000L)
-        clock.setCurrentTimeMillis(player2.third)
-        MediaPlayerData.addMediaPlayer(player2.first,
-                player2.second.copy(notificationKey = player2.first),
-                panel, clock, isSsReactivated = false)
-
-        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
-        // added to the front because it was active more recently.
-        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
-
-        val clickIntent3 = mock(PendingIntent::class.java)
-        val player3 = Triple("player3",
-                DATA.copy(clickIntent = clickIntent3),
-                500L)
-        clock.setCurrentTimeMillis(player3.third)
-        MediaPlayerData.addMediaPlayer(player3.first,
-                player3.second.copy(notificationKey = player3.first),
-                panel, clock, isSsReactivated = false)
-
-        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
-        // added to the end because it was active less recently.
-        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
-    }
-
-    @Test
-    fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
-        val delta = 0.0001F
-        val paginationSquishMiddle = TRANSFORM_BEZIER.getInterpolation(
-                (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION)
-        val paginationSquishEnd = TRANSFORM_BEZIER.getInterpolation(
-                (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION)
-        whenever(mediaHostStatesManager.mediaHostStates)
-            .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
-        whenever(mediaHostState.visible).thenReturn(true)
-        mediaCarouselController.currentEndLocation = LOCATION_QS
-        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
-        mediaCarouselController.updatePageIndicatorAlpha()
-        assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
-
-        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
-        mediaCarouselController.updatePageIndicatorAlpha()
-        assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
deleted file mode 100644
index 6e38d264..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.media
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
-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.mock
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-public class MediaPlayerDataTest : SysuiTestCase() {
-
-    @Mock
-    private lateinit var playerIsPlaying: MediaControlPanel
-    private var systemClock: FakeSystemClock = FakeSystemClock()
-
-    @JvmField
-    @Rule
-    val mockito = MockitoJUnit.rule()
-
-    companion object {
-        val LOCAL = MediaData.PLAYBACK_LOCAL
-        val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
-        val RESUMPTION = true
-        val PLAYING = true
-        val UNDETERMINED = null
-    }
-
-    @Before
-    fun setup() {
-        MediaPlayerData.clear()
-    }
-
-    @Test
-    fun addPlayingThenRemote() {
-        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsRemote = mock(MediaControlPanel::class.java)
-        val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer("2", dataIsRemote, playerIsRemote, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-
-        val players = MediaPlayerData.players()
-        assertThat(players).hasSize(2)
-        assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder()
-    }
-
-    @Test
-    fun switchPlayersPlaying() {
-        val playerIsPlaying1 = mock(MediaControlPanel::class.java)
-        var dataIsPlaying1 = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsPlaying2 = mock(MediaControlPanel::class.java)
-        var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-        MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION)
-        dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying1, playerIsPlaying1, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        MediaPlayerData.addMediaPlayer("2", dataIsPlaying2, playerIsPlaying2, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        val players = MediaPlayerData.players()
-        assertThat(players).hasSize(2)
-        assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder()
-    }
-
-    @Test
-    fun fullOrderTest() {
-        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
-        val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
-
-        val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
-        val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
-
-        val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
-        val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
-
-        val playerCanResume = mock(MediaControlPanel::class.java)
-        val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
-
-        val playerUndetermined = mock(MediaControlPanel::class.java)
-        val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
-
-        MediaPlayerData.addMediaPlayer(
-                "3", dataIsStoppedAndLocal, playerIsStoppedAndLocal, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer(
-                "5", dataIsStoppedAndRemote, playerIsStoppedAndRemote, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("4", dataCanResume, playerCanResume, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("1", dataIsPlaying, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer(
-                "2", dataIsPlayingAndRemote, playerIsPlayingAndRemote, systemClock,
-                isSsReactivated = false)
-        MediaPlayerData.addMediaPlayer("6", dataUndetermined, playerUndetermined, systemClock,
-                isSsReactivated = false)
-
-        val players = MediaPlayerData.players()
-        assertThat(players).hasSize(6)
-        assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote,
-            playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerUndetermined,
-            playerCanResume).inOrder()
-    }
-
-    @Test
-    fun testMoveMediaKeysAround() {
-        val keyA = "a"
-        val keyB = "b"
-
-        val data = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
-
-        assertThat(MediaPlayerData.players()).hasSize(0)
-
-        MediaPlayerData.addMediaPlayer(keyA, data, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        assertThat(MediaPlayerData.players()).hasSize(1)
-        MediaPlayerData.addMediaPlayer(keyB, data, playerIsPlaying, systemClock,
-                isSsReactivated = false)
-        systemClock.advanceTime(1)
-
-        assertThat(MediaPlayerData.players()).hasSize(2)
-
-        MediaPlayerData.moveIfExists(keyA, keyB)
-
-        assertThat(MediaPlayerData.players()).hasSize(1)
-
-        assertThat(MediaPlayerData.getMediaPlayer(keyA)).isNull()
-        assertThat(MediaPlayerData.getMediaPlayer(keyB)).isNotNull()
-    }
-
-    private fun createMediaData(
-        app: String,
-        isPlaying: Boolean?,
-        location: Int,
-        resumption: Boolean
-    ) = MediaTestUtils.emptyMediaData.copy(
-        app = app,
-        packageName = "package: $app",
-        playbackLocation = location,
-        resumption = resumption,
-        notificationKey = "key: $app",
-        isPlaying = isPlaying
-    )
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
deleted file mode 100644
index 3d9ed5f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTestUtils.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.android.systemui.media
-
-import com.android.internal.logging.InstanceId
-
-class MediaTestUtils {
-    companion object {
-        val emptyMediaData = MediaData(
-            userId = 0,
-            initialized = true,
-            app = null,
-            appIcon = null,
-            artist = null,
-            song = null,
-            artwork = null,
-            actions = emptyList(),
-            actionsToShowInCompact = emptyList(),
-            packageName = "",
-            token = null,
-            clickIntent = null,
-            device = null,
-            active = true,
-            resumeAction = null,
-            isPlaying = false,
-            instanceId = InstanceId.fakeInstanceId(-1),
-            appUid = -1)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt
deleted file mode 100644
index ee32793..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewHolderTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.android.systemui.media
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.view.LayoutInflater
-import android.widget.FrameLayout
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class MediaViewHolderTest : SysuiTestCase() {
-
-    @Test
-    fun create_succeeds() {
-        val inflater = LayoutInflater.from(context)
-        val parent = FrameLayout(context)
-
-        MediaViewHolder.create(inflater, parent)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt
new file mode 100644
index 0000000..3437365
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/MediaTestUtils.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls
+
+import com.android.internal.logging.InstanceId
+import com.android.systemui.media.controls.models.player.MediaData
+
+class MediaTestUtils {
+    companion object {
+        val emptyMediaData =
+            MediaData(
+                userId = 0,
+                initialized = true,
+                app = null,
+                appIcon = null,
+                artist = null,
+                song = null,
+                artwork = null,
+                actions = emptyList(),
+                actionsToShowInCompact = emptyList(),
+                packageName = "",
+                token = null,
+                clickIntent = null,
+                device = null,
+                active = true,
+                resumeAction = null,
+                isPlaying = false,
+                instanceId = InstanceId.fakeInstanceId(-1),
+                appUid = -1
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt
new file mode 100644
index 0000000..c829d4c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/MediaViewHolderTest.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.player
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaViewHolderTest : SysuiTestCase() {
+
+    @Test
+    fun create_succeeds() {
+        val inflater = LayoutInflater.from(context)
+        val parent = FrameLayout(context)
+
+        MediaViewHolder.create(inflater, parent)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
similarity index 92%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
index 9e9cda8..97b18e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.player
 
 import android.animation.Animator
 import android.animation.ObjectAnimator
@@ -26,6 +26,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.ui.SquigglyProgress
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
@@ -33,8 +34,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -56,10 +57,14 @@
 
     @Before
     fun setUp() {
-        context.orCreateTestableResources
-            .addOverride(R.dimen.qs_media_enabled_seekbar_height, enabledHeight)
-        context.orCreateTestableResources
-            .addOverride(R.dimen.qs_media_disabled_seekbar_height, disabledHeight)
+        context.orCreateTestableResources.addOverride(
+            R.dimen.qs_media_enabled_seekbar_height,
+            enabledHeight
+        )
+        context.orCreateTestableResources.addOverride(
+            R.dimen.qs_media_disabled_seekbar_height,
+            disabledHeight
+        )
 
         seekBarView = SeekBar(context)
         seekBarView.progressDrawable = mockSquigglyProgress
@@ -69,11 +74,12 @@
         whenever(mockHolder.scrubbingElapsedTimeView).thenReturn(scrubbingElapsedTimeView)
         whenever(mockHolder.scrubbingTotalTimeView).thenReturn(scrubbingTotalTimeView)
 
-        observer = object : SeekBarObserver(mockHolder) {
-            override fun buildResetAnimator(targetTime: Int): Animator {
-                return mockSeekbarAnimator
+        observer =
+            object : SeekBarObserver(mockHolder) {
+                override fun buildResetAnimator(targetTime: Int): Animator {
+                    return mockSeekbarAnimator
+                }
             }
-        }
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt
index 5973340..7cd8e74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarViewModelTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.models.player
 
 import android.media.MediaMetadata
 import android.media.session.MediaController
@@ -57,17 +57,18 @@
 
     private lateinit var viewModel: SeekBarViewModel
     private lateinit var fakeExecutor: FakeExecutor
-    private val taskExecutor: TaskExecutor = object : TaskExecutor() {
-        override fun executeOnDiskIO(runnable: Runnable) {
-            runnable.run()
+    private val taskExecutor: TaskExecutor =
+        object : TaskExecutor() {
+            override fun executeOnDiskIO(runnable: Runnable) {
+                runnable.run()
+            }
+            override fun postToMainThread(runnable: Runnable) {
+                runnable.run()
+            }
+            override fun isMainThread(): Boolean {
+                return true
+            }
         }
-        override fun postToMainThread(runnable: Runnable) {
-            runnable.run()
-        }
-        override fun isMainThread(): Boolean {
-            return true
-        }
-    }
     @Mock private lateinit var mockController: MediaController
     @Mock private lateinit var mockTransport: MediaController.TransportControls
     @Mock private lateinit var falsingManager: FalsingManager
@@ -81,7 +82,7 @@
     fun setUp() {
         fakeExecutor = FakeExecutor(FakeSystemClock())
         viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor), falsingManager)
-        viewModel.logSeek = { }
+        viewModel.logSeek = {}
         whenever(mockController.sessionToken).thenReturn(token1)
         whenever(mockBar.context).thenReturn(context)
 
@@ -135,16 +136,18 @@
     fun updateDurationWithPlayback() {
         // GIVEN that the duration is contained within the metadata
         val duration = 12000L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
         whenever(mockController.getMetadata()).thenReturn(metadata)
         // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -158,10 +161,11 @@
     fun updateDurationWithoutPlayback() {
         // GIVEN that the duration is contained within the metadata
         val duration = 12000L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
         whenever(mockController.getMetadata()).thenReturn(metadata)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -174,16 +178,18 @@
     fun updateDurationNegative() {
         // GIVEN that the duration is negative
         val duration = -1L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
         whenever(mockController.getMetadata()).thenReturn(metadata)
         // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -195,16 +201,18 @@
     fun updateDurationZero() {
         // GIVEN that the duration is zero
         val duration = 0L
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
-            build()
-        }
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
+                build()
+            }
         whenever(mockController.getMetadata()).thenReturn(metadata)
         // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -218,10 +226,11 @@
         // GIVEN that the metadata is null
         whenever(mockController.getMetadata()).thenReturn(null)
         // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -233,10 +242,11 @@
     fun updateElapsedTime() {
         // GIVEN that the PlaybackState contains the current position
         val position = 200L
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, position, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, position, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -248,10 +258,11 @@
     @Ignore
     fun updateSeekAvailable() {
         // GIVEN that seek is included in actions
-        val state = PlaybackState.Builder().run {
-            setActions(PlaybackState.ACTION_SEEK_TO)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setActions(PlaybackState.ACTION_SEEK_TO)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -263,10 +274,11 @@
     @Ignore
     fun updateSeekNotAvailable() {
         // GIVEN that seek is not included in actions
-        val state = PlaybackState.Builder().run {
-            setActions(PlaybackState.ACTION_PLAY)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setActions(PlaybackState.ACTION_PLAY)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -318,9 +330,7 @@
     @Ignore
     fun onSeekProgressWithSeekStarting() {
         val pos = 42L
-        with(viewModel) {
-            onSeekProgress(pos)
-        }
+        with(viewModel) { onSeekProgress(pos) }
         fakeExecutor.runAllReady()
         // THEN then elapsed time should not be updated
         assertThat(viewModel.progress.value!!.elapsedTime).isNull()
@@ -329,11 +339,12 @@
     @Test
     fun seekStarted_listenerNotified() {
         var isScrubbing: Boolean? = null
-        val listener = object : SeekBarViewModel.ScrubbingChangeListener {
-            override fun onScrubbingChanged(scrubbing: Boolean) {
-                isScrubbing = scrubbing
+        val listener =
+            object : SeekBarViewModel.ScrubbingChangeListener {
+                override fun onScrubbingChanged(scrubbing: Boolean) {
+                    isScrubbing = scrubbing
+                }
             }
-        }
         viewModel.setScrubbingChangeListener(listener)
 
         viewModel.onSeekStarting()
@@ -345,11 +356,12 @@
     @Test
     fun seekEnded_listenerNotified() {
         var isScrubbing: Boolean? = null
-        val listener = object : SeekBarViewModel.ScrubbingChangeListener {
-            override fun onScrubbingChanged(scrubbing: Boolean) {
-                isScrubbing = scrubbing
+        val listener =
+            object : SeekBarViewModel.ScrubbingChangeListener {
+                override fun onScrubbingChanged(scrubbing: Boolean) {
+                    isScrubbing = scrubbing
+                }
             }
-        }
         viewModel.setScrubbingChangeListener(listener)
 
         // Start seeking
@@ -385,9 +397,7 @@
         val bar = SeekBar(context)
 
         // WHEN we get an onProgressChanged event without an onStartTrackingTouch event
-        with(viewModel.seekBarListener) {
-            onProgressChanged(bar, pos, true)
-        }
+        with(viewModel.seekBarListener) { onProgressChanged(bar, pos, true) }
         fakeExecutor.runAllReady()
 
         // THEN we immediately update the transport
@@ -412,9 +422,7 @@
         viewModel.updateController(mockController)
         // WHEN user starts dragging the seek bar
         val pos = 42
-        val bar = SeekBar(context).apply {
-            progress = pos
-        }
+        val bar = SeekBar(context).apply { progress = pos }
         viewModel.seekBarListener.onStartTrackingTouch(bar)
         fakeExecutor.runAllReady()
         // THEN transport controls should be used
@@ -427,9 +435,7 @@
         viewModel.updateController(mockController)
         // WHEN user ends drag
         val pos = 42
-        val bar = SeekBar(context).apply {
-            progress = pos
-        }
+        val bar = SeekBar(context).apply { progress = pos }
         viewModel.seekBarListener.onStopTrackingTouch(bar)
         fakeExecutor.runAllReady()
         // THEN transport controls should be used
@@ -443,9 +449,7 @@
         // WHEN user starts dragging the seek bar
         val pos = 42
         val progPos = 84
-        val bar = SeekBar(context).apply {
-            progress = pos
-        }
+        val bar = SeekBar(context).apply { progress = pos }
         with(viewModel.seekBarListener) {
             onStartTrackingTouch(bar)
             onProgressChanged(bar, progPos, true)
@@ -478,10 +482,11 @@
     @Test
     fun queuePollTaskWhenPlaying() {
         // GIVEN that the track is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 100L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 100L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN the controller is updated
         viewModel.updateController(mockController)
@@ -492,10 +497,11 @@
     @Test
     fun noQueuePollTaskWhenStopped() {
         // GIVEN that the playback state is stopped
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_STOPPED, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_STOPPED, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN updated
         viewModel.updateController(mockController)
@@ -512,10 +518,11 @@
             runAllReady()
         }
         // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN updated
         viewModel.updateController(mockController)
@@ -532,10 +539,11 @@
             runAllReady()
         }
         // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // WHEN updated
         viewModel.updateController(mockController)
@@ -546,10 +554,11 @@
     @Test
     fun pollTaskQueuesAnotherPollTaskWhenPlaying() {
         // GIVEN that the track is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 100L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 100L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         viewModel.updateController(mockController)
         // WHEN the next task runs
@@ -566,10 +575,11 @@
         // GIVEN listening
         viewModel.listening = true
         // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         viewModel.updateController(mockController)
         with(fakeExecutor) {
@@ -592,10 +602,11 @@
         // GIVEN listening
         viewModel.listening = true
         // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         viewModel.updateController(mockController)
         with(fakeExecutor) {
@@ -621,10 +632,11 @@
         // GIVEN listening
         viewModel.listening = true
         // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         viewModel.updateController(mockController)
         with(fakeExecutor) {
@@ -654,10 +666,11 @@
             runAllReady()
         }
         // AND the playback state is playing
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_STOPPED, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_STOPPED, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         viewModel.updateController(mockController)
         // WHEN start listening
@@ -673,10 +686,11 @@
         verify(mockController).registerCallback(captor.capture())
         val callback = captor.value
         // WHEN the callback receives an new state
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 100L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 100L, 1f)
+                build()
+            }
         callback.onPlaybackStateChanged(state)
         with(fakeExecutor) {
             advanceClockToNext()
@@ -690,16 +704,18 @@
     @Ignore
     fun clearSeekBar() {
         // GIVEN that the duration is contained within the metadata
-        val metadata = MediaMetadata.Builder().run {
-            putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
-            build()
-        }
+        val metadata =
+            MediaMetadata.Builder().run {
+                putLong(MediaMetadata.METADATA_KEY_DURATION, 12000L)
+                build()
+            }
         whenever(mockController.getMetadata()).thenReturn(metadata)
         // AND a valid playback state (ie. media session is not destroyed)
-        val state = PlaybackState.Builder().run {
-            setState(PlaybackState.STATE_PLAYING, 200L, 1f)
-            build()
-        }
+        val state =
+            PlaybackState.Builder().run {
+                setState(PlaybackState.STATE_PLAYING, 200L, 1f)
+                build()
+            }
         whenever(mockController.getPlaybackState()).thenReturn(state)
         // AND the controller has been updated
         viewModel.updateController(mockController)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
similarity index 62%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
index b5078bc..1d6e980 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SmartspaceMediaDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/recommendation/SmartspaceMediaDataTest.kt
@@ -1,4 +1,20 @@
-package com.android.systemui.media
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.models.recommendation
 
 import android.app.smartspace.SmartspaceAction
 import android.graphics.drawable.Icon
@@ -36,11 +52,11 @@
 
     @Test
     fun isValid_tooFewRecs_returnsFalse() {
-        val data = DEFAULT_DATA.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
+        val data =
+            DEFAULT_DATA.copy(
+                recommendations =
+                    listOf(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
             )
-        )
 
         assertThat(data.isValid()).isFalse()
     }
@@ -50,14 +66,10 @@
         val recommendations = mutableListOf<SmartspaceAction>()
         // Add one fewer recommendation w/ icon than the number required
         for (i in 1 until NUM_REQUIRED_RECOMMENDATIONS) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
         }
         for (i in 1 until 3) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(null).build()
-            )
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(null).build())
         }
 
         val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -70,9 +82,7 @@
         val recommendations = mutableListOf<SmartspaceAction>()
         // Add the number of required recommendations
         for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
         }
 
         val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -85,9 +95,7 @@
         val recommendations = mutableListOf<SmartspaceAction>()
         // Add more than enough recommendations
         for (i in 0 until NUM_REQUIRED_RECOMMENDATIONS + 3) {
-            recommendations.add(
-                SmartspaceAction.Builder("id", "title").setIcon(icon).build()
-            )
+            recommendations.add(SmartspaceAction.Builder("id", "title").setIcon(icon).build())
         }
 
         val data = DEFAULT_DATA.copy(recommendations = recommendations)
@@ -96,13 +104,14 @@
     }
 }
 
-private val DEFAULT_DATA = SmartspaceMediaData(
-    targetId = "INVALID",
-    isActive = false,
-    packageName = "INVALID",
-    cardAction = null,
-    recommendations = emptyList(),
-    dismissIntent = null,
-    headphoneConnectionTimeMillis = 0,
-    instanceId = InstanceId.fakeInstanceId(-1)
-)
+private val DEFAULT_DATA =
+    SmartspaceMediaData(
+        targetId = "INVALID",
+        isActive = false,
+        packageName = "INVALID",
+        cardAction = null,
+        recommendations = emptyList(),
+        dismissIntent = null,
+        headphoneConnectionTimeMillis = 0,
+        instanceId = InstanceId.fakeInstanceId(-1)
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
index 04b93d7..4d2d0f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media;
+package com.android.systemui.media.controls.pipeline;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -26,7 +26,6 @@
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 
-import android.graphics.Color;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
@@ -34,6 +33,8 @@
 
 import com.android.internal.logging.InstanceId;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.models.player.MediaDeviceData;
 
 import org.junit.Before;
 import org.junit.Rule;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
similarity index 82%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
index 6468fe1..575b1c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataFilterTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.app.smartspace.SmartspaceAction
 import android.testing.AndroidTestingRunner
@@ -24,6 +24,11 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.ui.MediaPlayerData
+import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -58,24 +63,15 @@
 @TestableLooper.RunWithLooper
 class MediaDataFilterTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var listener: MediaDataManager.Listener
-    @Mock
-    private lateinit var broadcastDispatcher: BroadcastDispatcher
-    @Mock
-    private lateinit var broadcastSender: BroadcastSender
-    @Mock
-    private lateinit var mediaDataManager: MediaDataManager
-    @Mock
-    private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
-    @Mock
-    private lateinit var executor: Executor
-    @Mock
-    private lateinit var smartspaceData: SmartspaceMediaData
-    @Mock
-    private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
-    @Mock
-    private lateinit var logger: MediaUiEventLogger
+    @Mock private lateinit var listener: MediaDataManager.Listener
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var broadcastSender: BroadcastSender
+    @Mock private lateinit var mediaDataManager: MediaDataManager
+    @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager
+    @Mock private lateinit var executor: Executor
+    @Mock private lateinit var smartspaceData: SmartspaceMediaData
+    @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
+    @Mock private lateinit var logger: MediaUiEventLogger
 
     private lateinit var mediaDataFilter: MediaDataFilter
     private lateinit var dataMain: MediaData
@@ -86,14 +82,16 @@
     fun setup() {
         MockitoAnnotations.initMocks(this)
         MediaPlayerData.clear()
-        mediaDataFilter = MediaDataFilter(
-            context,
-            broadcastDispatcher,
-            broadcastSender,
-            lockscreenUserManager,
-            executor,
-            clock,
-            logger)
+        mediaDataFilter =
+            MediaDataFilter(
+                context,
+                broadcastDispatcher,
+                broadcastSender,
+                lockscreenUserManager,
+                executor,
+                clock,
+                logger
+            )
         mediaDataFilter.mediaDataManager = mediaDataManager
         mediaDataFilter.addListener(listener)
 
@@ -101,11 +99,13 @@
         setUser(USER_MAIN)
 
         // Set up test media data
-        dataMain = MediaTestUtils.emptyMediaData.copy(
+        dataMain =
+            MediaTestUtils.emptyMediaData.copy(
                 userId = USER_MAIN,
                 packageName = PACKAGE,
                 instanceId = INSTANCE_ID,
-                appUid = APP_UID)
+                appUid = APP_UID
+            )
         dataGuest = dataMain.copy(userId = USER_GUEST)
 
         `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
@@ -113,8 +113,8 @@
         `when`(smartspaceData.isValid()).thenReturn(true)
         `when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
         `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
-        `when`(smartspaceData.headphoneConnectionTimeMillis).thenReturn(
-                clock.currentTimeMillis() - 100)
+        `when`(smartspaceData.headphoneConnectionTimeMillis)
+            .thenReturn(clock.currentTimeMillis() - 100)
         `when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
     }
 
@@ -130,8 +130,8 @@
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
         // THEN we should tell the listener
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -140,8 +140,8 @@
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest)
 
         // THEN we should NOT tell the listener
-        verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(),
-                anyInt(), anyBoolean())
+        verify(listener, never())
+            .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
     @Test
@@ -187,12 +187,12 @@
         setUser(USER_GUEST)
 
         // THEN we should add back the guest user media
-        verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
 
         // but not the main user's
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(),
-                anyInt(), anyBoolean())
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean())
     }
 
     @Test
@@ -340,7 +340,7 @@
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         verify(listener)
-                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
         assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
         verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
@@ -353,8 +353,8 @@
 
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-        verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean(),
-                anyInt(), anyBoolean())
+        verify(listener, never())
+            .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
         verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
         assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
@@ -370,7 +370,7 @@
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         verify(listener)
-                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
         assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
         verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
@@ -400,15 +400,15 @@
         // WHEN we have media that was recently played, but not currently active
         val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
         // AND we get a smartspace signal
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         // THEN we should tell listeners to treat the media as not active instead
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(),
-                anyInt(), anyBoolean())
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
         verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
         assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
@@ -423,16 +423,23 @@
         // WHEN we have media that was recently played, but not currently active
         val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
         // AND we get a smartspace signal
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         // THEN we should tell listeners to treat the media as active instead
         val dataCurrentAndActive = dataCurrent.copy(active = true)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
-                eq(100), eq(true))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
         // Smartspace update shouldn't be propagated for the empty rec list.
         verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
@@ -445,20 +452,27 @@
         // WHEN we have media that was recently played, but not currently active
         val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
         // AND we get a smartspace signal
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         // THEN we should tell listeners to treat the media as active instead
         val dataCurrentAndActive = dataCurrent.copy(active = true)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
-                eq(100), eq(true))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
         assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
         // Smartspace update should also be propagated but not prioritized.
         verify(listener)
-                .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
         verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
         verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
     }
@@ -477,14 +491,21 @@
     fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() {
         val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
         mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
 
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         val dataCurrentAndActive = dataCurrent.copy(active = true)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true),
-                eq(100), eq(true))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                eq(dataCurrentAndActive),
+                eq(true),
+                eq(100),
+                eq(true)
+            )
 
         mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
similarity index 60%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index f9c7d2d..11eb26b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -1,4 +1,20 @@
-package com.android.systemui.media
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.pipeline
 
 import android.app.Notification
 import android.app.Notification.MediaStyle
@@ -26,6 +42,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
+import com.android.systemui.media.controls.resume.MediaResumeListener
+import com.android.systemui.media.controls.util.MediaControllerFactory
+import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.SbnBuilder
 import com.android.systemui.tuner.TunerService
@@ -111,58 +134,68 @@
 
     private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
 
-    private val originalSmartspaceSetting = Settings.Secure.getInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
+    private val originalSmartspaceSetting =
+        Settings.Secure.getInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
+        )
 
     @Before
     fun setup() {
         foregroundExecutor = FakeExecutor(clock)
         backgroundExecutor = FakeExecutor(clock)
         smartspaceMediaDataProvider = SmartspaceMediaDataProvider()
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 1)
-        mediaDataManager = MediaDataManager(
-            context = context,
-            backgroundExecutor = backgroundExecutor,
-            foregroundExecutor = foregroundExecutor,
-            mediaControllerFactory = mediaControllerFactory,
-            broadcastDispatcher = broadcastDispatcher,
-            dumpManager = dumpManager,
-            mediaTimeoutListener = mediaTimeoutListener,
-            mediaResumeListener = mediaResumeListener,
-            mediaSessionBasedFilter = mediaSessionBasedFilter,
-            mediaDeviceManager = mediaDeviceManager,
-            mediaDataCombineLatest = mediaDataCombineLatest,
-            mediaDataFilter = mediaDataFilter,
-            activityStarter = activityStarter,
-            smartspaceMediaDataProvider = smartspaceMediaDataProvider,
-            useMediaResumption = true,
-            useQsMediaPlayer = true,
-            systemClock = clock,
-            tunerService = tunerService,
-            mediaFlags = mediaFlags,
-            logger = logger
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            1
         )
-        verify(tunerService).addTunable(capture(tunableCaptor),
-                eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
+        mediaDataManager =
+            MediaDataManager(
+                context = context,
+                backgroundExecutor = backgroundExecutor,
+                foregroundExecutor = foregroundExecutor,
+                mediaControllerFactory = mediaControllerFactory,
+                broadcastDispatcher = broadcastDispatcher,
+                dumpManager = dumpManager,
+                mediaTimeoutListener = mediaTimeoutListener,
+                mediaResumeListener = mediaResumeListener,
+                mediaSessionBasedFilter = mediaSessionBasedFilter,
+                mediaDeviceManager = mediaDeviceManager,
+                mediaDataCombineLatest = mediaDataCombineLatest,
+                mediaDataFilter = mediaDataFilter,
+                activityStarter = activityStarter,
+                smartspaceMediaDataProvider = smartspaceMediaDataProvider,
+                useMediaResumption = true,
+                useQsMediaPlayer = true,
+                systemClock = clock,
+                tunerService = tunerService,
+                mediaFlags = mediaFlags,
+                logger = logger
+            )
+        verify(tunerService)
+            .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
         session = MediaSession(context, "MediaDataManagerTestSession")
-        mediaNotification = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+        mediaNotification =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                }
+                build()
             }
-            build()
-        }
-        metadataBuilder = MediaMetadata.Builder().apply {
-            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
-            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
-        }
+        metadataBuilder =
+            MediaMetadata.Builder().apply {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+            }
         whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
         whenever(controller.transportControls).thenReturn(transportControls)
         whenever(controller.playbackInfo).thenReturn(playbackInfo)
-        whenever(playbackInfo.playbackType).thenReturn(
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL)
 
         // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
         // listeners in the internal processing pipeline. It receives events, but ince it is a
@@ -170,18 +203,18 @@
         // treat mediaSessionBasedFilter as a listener for testing.
         listener = mediaSessionBasedFilter
 
-        val recommendationExtras = Bundle().apply {
-            putString("package_name", PACKAGE_NAME)
-            putParcelable("dismiss_intent", DISMISS_INTENT)
-        }
+        val recommendationExtras =
+            Bundle().apply {
+                putString("package_name", PACKAGE_NAME)
+                putParcelable("dismiss_intent", DISMISS_INTENT)
+            }
         val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play)
         whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
         whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
         whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras)
         whenever(mediaRecommendationItem.icon).thenReturn(icon)
-        validRecommendationList = listOf(
-            mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem
-        )
+        validRecommendationList =
+            listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem)
         whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
         whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
         whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
@@ -194,8 +227,11 @@
     fun tearDown() {
         session.release()
         mediaDataManager.destroy()
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, originalSmartspaceSetting)
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            originalSmartspaceSetting
+        )
     }
 
     @Test
@@ -212,21 +248,36 @@
     @Test
     fun testSetTimedOut_resume_dismissesMedia() {
         // WHEN resume controls are present, and time out
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
-                APP_NAME, pendingIntent, PACKAGE_NAME)
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
 
         backgroundExecutor.runAllReady()
         foregroundExecutor.runAllReady()
-        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor),
-            eq(true), eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
 
         mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true)
-        verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId))
+        verify(logger)
+            .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId))
 
         // THEN it is removed and listeners are informed
         foregroundExecutor.advanceClockToLast()
@@ -243,8 +294,13 @@
     @Test
     fun testOnMetaDataLoaded_callsListener() {
         addNotificationAndLoad()
-        verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_LOCAL))
+        verify(logger)
+            .logActiveMediaAdded(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId),
+                eq(MediaData.PLAYBACK_LOCAL)
+            )
     }
 
     @Test
@@ -255,56 +311,85 @@
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value!!.active).isTrue()
     }
 
     @Test
     fun testOnNotificationAdded_isRcn_markedRemote() {
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
             }
-            build()
-        }
 
         mediaDataManager.onNotificationAdded(KEY, rcn)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-        assertThat(mediaDataCaptor.value!!.playbackLocation).isEqualTo(
-                MediaData.PLAYBACK_CAST_REMOTE)
-        verify(logger).logActiveMediaAdded(anyInt(), eq(SYSTEM_PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value!!.playbackLocation)
+            .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE)
+        verify(logger)
+            .logActiveMediaAdded(
+                anyInt(),
+                eq(SYSTEM_PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId),
+                eq(MediaData.PLAYBACK_CAST_REMOTE)
+            )
     }
 
     @Test
     fun testOnNotificationAdded_hasSubstituteName_isUsed() {
         val subName = "Substitute Name"
-        val notif = SbnBuilder().run {
-            modifyNotification(context).also {
-                it.extras = Bundle().apply {
-                    putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName)
+        val notif =
+            SbnBuilder().run {
+                modifyNotification(context).also {
+                    it.extras =
+                        Bundle().apply {
+                            putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName)
+                        }
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
                 }
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                })
+                build()
             }
-            build()
-        }
 
         mediaDataManager.onNotificationAdded(KEY, notif)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-            eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
 
         assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName)
     }
@@ -314,17 +399,18 @@
         val bundle = Bundle()
         // wrong data type
         bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle())
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.addExtras(bundle)
-                it.setStyle(MediaStyle().apply {
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.addExtras(bundle)
+                    it.setStyle(
+                        MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) }
+                    )
+                }
+                build()
             }
-            build()
-        }
 
         mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
         // no crash even though the data structure is incorrect
@@ -335,18 +421,21 @@
         val bundle = Bundle()
         // wrong data type
         bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle())
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.addExtras(bundle)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.addExtras(bundle)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
             }
-            build()
-        }
 
         mediaDataManager.loadMediaDataInBg(KEY, rcn, null)
         // no crash even though the data structure is incorrect
@@ -373,8 +462,14 @@
         mediaDataManager.onNotificationRemoved(KEY)
         // THEN the media data indicates that it is for resumption
         verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
         assertThat(mediaDataCaptor.value.isPlaying).isFalse()
         verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -389,8 +484,14 @@
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
         verify(listener)
-            .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
         val resumableData = data.copy(resumeAction = Runnable {})
@@ -401,8 +502,14 @@
         mediaDataManager.onNotificationRemoved(KEY)
         // THEN the data is for resumption and the key is migrated to the package name
         verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
         verify(listener, never()).onMediaDataRemoved(eq(KEY))
         // WHEN the second is removed
@@ -410,8 +517,13 @@
         // THEN the data is for resumption and the second key is removed
         verify(listener)
             .onMediaDataLoaded(
-                eq(PACKAGE_NAME), eq(PACKAGE_NAME), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+                eq(PACKAGE_NAME),
+                eq(PACKAGE_NAME),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
         verify(listener).onMediaDataRemoved(eq(KEY_2))
     }
@@ -420,15 +532,20 @@
     fun testOnNotificationRemoved_withResumption_butNotLocal() {
         // GIVEN that the manager has a notification with a resume action, but is not local
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        whenever(playbackInfo.playbackType).thenReturn(
-                MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
         addNotificationAndLoad()
         val data = mediaDataCaptor.value
-        val dataRemoteWithResume = data.copy(resumeAction = Runnable {},
-                playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
+        val dataRemoteWithResume =
+            data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
         mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume)
-        verify(logger).logActiveMediaAdded(anyInt(), eq(PACKAGE_NAME),
-            eq(mediaDataCaptor.value.instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL))
+        verify(logger)
+            .logActiveMediaAdded(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(mediaDataCaptor.value.instanceId),
+                eq(MediaData.PLAYBACK_CAST_LOCAL)
+            )
 
         // WHEN the notification is removed
         mediaDataManager.onNotificationRemoved(KEY)
@@ -440,19 +557,33 @@
     @Test
     fun testAddResumptionControls() {
         // WHEN resumption controls are added
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
         val currentTime = clock.elapsedRealtime()
-        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
-                APP_NAME, pendingIntent, PACKAGE_NAME)
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         // THEN the media data indicates that it is for resumption
         verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         val data = mediaDataCaptor.value
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
@@ -466,16 +597,31 @@
     @Test
     fun testResumptionDisabled_dismissesResumeControls() {
         // WHEN there are resume controls and resumption is switched off
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken,
-            APP_NAME, pendingIntent, PACKAGE_NAME)
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        mediaDataManager.addResumptionControls(
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
+        )
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor),
-            eq(true), eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         val data = mediaDataCaptor.value
         mediaDataManager.setMediaResumptionEnabled(false)
 
@@ -508,23 +654,30 @@
     fun testBadArtwork_doesNotUse() {
         // WHEN notification has a too-small artwork
         val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
-        val notif = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-                it.setLargeIcon(artwork)
+        val notif =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    it.setLargeIcon(artwork)
+                }
+                build()
             }
-            build()
-        }
         mediaDataManager.onNotificationAdded(KEY, notif)
 
         // THEN it still loads
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         verify(listener)
-            .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
     }
 
     @Test
@@ -533,18 +686,23 @@
         verify(logger).getNewInstanceId()
         val instanceId = instanceIdSequence.lastInstanceId
 
-        verify(listener).onSmartspaceMediaDataLoaded(
-            eq(KEY_MEDIA_SMARTSPACE),
-            eq(SmartspaceMediaData(
-                targetId = KEY_MEDIA_SMARTSPACE,
-                isActive = true,
-                packageName = PACKAGE_NAME,
-                cardAction = mediaSmartspaceBaseAction,
-                recommendations = validRecommendationList,
-                dismissIntent = DISMISS_INTENT,
-                headphoneConnectionTimeMillis = 1234L,
-                instanceId = InstanceId.fakeInstanceId(instanceId))),
-            eq(false))
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    SmartspaceMediaData(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        packageName = PACKAGE_NAME,
+                        cardAction = mediaSmartspaceBaseAction,
+                        recommendations = validRecommendationList,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = 1234L,
+                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                    )
+                ),
+                eq(false)
+            )
     }
 
     @Test
@@ -554,23 +712,29 @@
         verify(logger).getNewInstanceId()
         val instanceId = instanceIdSequence.lastInstanceId
 
-        verify(listener).onSmartspaceMediaDataLoaded(
-            eq(KEY_MEDIA_SMARTSPACE),
-            eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = KEY_MEDIA_SMARTSPACE,
-                isActive = true,
-                dismissIntent = DISMISS_INTENT,
-                headphoneConnectionTimeMillis = 1234L,
-                instanceId = InstanceId.fakeInstanceId(instanceId))),
-            eq(false))
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        dismissIntent = DISMISS_INTENT,
+                        headphoneConnectionTimeMillis = 1234L,
+                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                    )
+                ),
+                eq(false)
+            )
     }
 
     @Test
     fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() {
-        val recommendationExtras = Bundle().apply {
-            putString("package_name", PACKAGE_NAME)
-            putParcelable("dismiss_intent", null)
-        }
+        val recommendationExtras =
+            Bundle().apply {
+                putString("package_name", PACKAGE_NAME)
+                putParcelable("dismiss_intent", null)
+            }
         whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras)
         whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction)
         whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf())
@@ -579,15 +743,20 @@
         verify(logger).getNewInstanceId()
         val instanceId = instanceIdSequence.lastInstanceId
 
-        verify(listener).onSmartspaceMediaDataLoaded(
-            eq(KEY_MEDIA_SMARTSPACE),
-            eq(EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-                targetId = KEY_MEDIA_SMARTSPACE,
-                isActive = true,
-                dismissIntent = null,
-                headphoneConnectionTimeMillis = 1234L,
-                instanceId = InstanceId.fakeInstanceId(instanceId))),
-            eq(false))
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(
+                eq(KEY_MEDIA_SMARTSPACE),
+                eq(
+                    EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                        targetId = KEY_MEDIA_SMARTSPACE,
+                        isActive = true,
+                        dismissIntent = null,
+                        headphoneConnectionTimeMillis = 1234L,
+                        instanceId = InstanceId.fakeInstanceId(instanceId)
+                    )
+                ),
+                eq(false)
+            )
     }
 
     @Test
@@ -595,7 +764,7 @@
         smartspaceMediaDataProvider.onTargetsAvailable(listOf())
         verify(logger, never()).getNewInstanceId()
         verify(listener, never())
-                .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
+            .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
     }
 
     @Ignore("b/233283726")
@@ -615,15 +784,18 @@
     @Test
     fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
         // WHEN media recommendation setting is off
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            0
+        )
         tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
 
         smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
 
         // THEN smartspace signal is ignored
         verify(listener, never())
-                .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
+            .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean())
     }
 
     @Ignore("b/229838140")
@@ -631,12 +803,15 @@
     fun testMediaRecommendationDisabled_removesSmartspaceData() {
         // GIVEN a media recommendation card is present
         smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
-        verify(listener).onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(),
-                anyBoolean())
+        verify(listener)
+            .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean())
 
         // WHEN the media recommendation setting is turned off
-        Settings.Secure.putInt(context.contentResolver,
-                Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0)
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+            0
+        )
         tunableCaptor.value.onTuningChanged(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, "0")
 
         // THEN listeners are notified
@@ -665,8 +840,15 @@
         mediaDataManager.setTimedOut(KEY, true, true)
 
         // THEN the last active time is not changed
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
     }
 
@@ -687,8 +869,14 @@
 
         // THEN the last active time is not changed
         verify(listener)
-            .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
-                    eq(0), eq(false))
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.resumption).isTrue()
         assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime)
 
@@ -700,17 +888,20 @@
     @Test
     fun testTooManyCompactActions_isTruncated() {
         // GIVEN a notification where too many compact actions were specified
-        val notif = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setShowActionsInCompactView(0, 1, 2, 3, 4)
-                })
+        val notif =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setShowActionsInCompactView(0, 1, 2, 3, 4)
+                        }
+                    )
+                }
+                build()
             }
-            build()
-        }
 
         // WHEN the notification is loaded
         mediaDataManager.onNotificationAdded(KEY, notif)
@@ -718,29 +909,35 @@
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
 
         // THEN only the first MAX_COMPACT_ACTIONS are actually set
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo(
-                MediaDataManager.MAX_COMPACT_ACTIONS)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.actionsToShowInCompact.size)
+            .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS)
     }
 
     @Test
     fun testTooManyNotificationActions_isTruncated() {
         // GIVEN a notification where too many notification actions are added
         val action = Notification.Action(R.drawable.ic_android, "action", null)
-        val notif = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                })
-                for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
-                    it.addAction(action)
+        val notif =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) {
+                        it.addAction(action)
+                    }
                 }
+                build()
             }
-            build()
-        }
 
         // WHEN the notification is loaded
         mediaDataManager.onNotificationAdded(KEY, notif)
@@ -748,10 +945,17 @@
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
 
         // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-            eq(0), eq(false))
-        assertThat(mediaDataCaptor.value.actions.size).isEqualTo(
-            MediaDataManager.MAX_NOTIFICATION_ACTIONS)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.actions.size)
+            .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS)
     }
 
     @Test
@@ -760,21 +964,29 @@
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
         whenever(controller.playbackState).thenReturn(null)
 
-        val notifWithAction = SbnBuilder().run {
-            setPkg(PACKAGE_NAME)
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
-                it.addAction(android.R.drawable.ic_media_play, desc, null)
+        val notifWithAction =
+            SbnBuilder().run {
+                setPkg(PACKAGE_NAME)
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) })
+                    it.addAction(android.R.drawable.ic_media_play, desc, null)
+                }
+                build()
             }
-            build()
-        }
         mediaDataManager.onNotificationAdded(KEY, notifWithAction)
 
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-                eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
 
         assertThat(mediaDataCaptor.value!!.semanticActions).isNull()
         assertThat(mediaDataCaptor.value!!.actions).hasSize(1)
@@ -785,11 +997,11 @@
     fun testPlaybackActions_hasPrevNext() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val stateActions = PlaybackState.ACTION_PLAY or
+        val stateActions =
+            PlaybackState.ACTION_PLAY or
                 PlaybackState.ACTION_SKIP_TO_PREVIOUS or
                 PlaybackState.ACTION_SKIP_TO_NEXT
-        val stateBuilder = PlaybackState.Builder()
-                .setActions(stateActions)
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
         customDesc.forEach {
             stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
         }
@@ -801,20 +1013,20 @@
         val actions = mediaDataCaptor.value!!.semanticActions!!
 
         assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_play))
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
         actions.playOrPause!!.action!!.run()
         verify(transportControls).play()
 
         assertThat(actions.prevOrCustom).isNotNull()
-        assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_prev))
+        assertThat(actions.prevOrCustom!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_prev))
         actions.prevOrCustom!!.action!!.run()
         verify(transportControls).skipToPrevious()
 
         assertThat(actions.nextOrCustom).isNotNull()
-        assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_next))
+        assertThat(actions.nextOrCustom!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_next))
         actions.nextOrCustom!!.action!!.run()
         verify(transportControls).skipToNext()
 
@@ -830,8 +1042,7 @@
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5")
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
         val stateActions = PlaybackState.ACTION_PLAY
-        val stateBuilder = PlaybackState.Builder()
-                .setActions(stateActions)
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
         customDesc.forEach {
             stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
         }
@@ -843,8 +1054,8 @@
         val actions = mediaDataCaptor.value!!.semanticActions!!
 
         assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_play))
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
 
         assertThat(actions.prevOrCustom).isNotNull()
         assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0])
@@ -863,7 +1074,8 @@
     fun testPlaybackActions_connecting() {
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
         val stateActions = PlaybackState.ACTION_PLAY
-        val stateBuilder = PlaybackState.Builder()
+        val stateBuilder =
+            PlaybackState.Builder()
                 .setState(PlaybackState.STATE_BUFFERING, 0, 10f)
                 .setActions(stateActions)
         whenever(controller.playbackState).thenReturn(stateBuilder.build())
@@ -874,8 +1086,8 @@
         val actions = mediaDataCaptor.value!!.semanticActions!!
 
         assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_connecting))
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_connecting))
     }
 
     @Test
@@ -883,15 +1095,15 @@
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
         val stateActions = PlaybackState.ACTION_PLAY
-        val stateBuilder = PlaybackState.Builder()
-                .setActions(stateActions)
+        val stateBuilder = PlaybackState.Builder().setActions(stateActions)
         customDesc.forEach {
             stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause)
         }
-        val extras = Bundle().apply {
-            putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
-            putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
-        }
+        val extras =
+            Bundle().apply {
+                putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
+                putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
+            }
         whenever(controller.playbackState).thenReturn(stateBuilder.build())
         whenever(controller.extras).thenReturn(extras)
 
@@ -901,8 +1113,8 @@
         val actions = mediaDataCaptor.value!!.semanticActions!!
 
         assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-                context.getString(R.string.controls_media_button_play))
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
 
         assertThat(actions.prevOrCustom).isNull()
         assertThat(actions.nextOrCustom).isNull()
@@ -930,8 +1142,8 @@
         val actions = mediaDataCaptor.value!!.semanticActions!!
 
         assertThat(actions.playOrPause).isNotNull()
-        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
-            context.getString(R.string.controls_media_button_play))
+        assertThat(actions.playOrPause!!.contentDescription)
+            .isEqualTo(context.getString(R.string.controls_media_button_play))
         actions.playOrPause!!.action!!.run()
         verify(transportControls).play()
     }
@@ -944,30 +1156,43 @@
 
         // Location is updated to local cast
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
-        whenever(playbackInfo.playbackType).thenReturn(
-            MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        whenever(playbackInfo.playbackType)
+            .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE)
         addNotificationAndLoad()
-        verify(logger).logPlaybackLocationChange(anyInt(), eq(PACKAGE_NAME),
-            eq(instanceId), eq(MediaData.PLAYBACK_CAST_LOCAL))
+        verify(logger)
+            .logPlaybackLocationChange(
+                anyInt(),
+                eq(PACKAGE_NAME),
+                eq(instanceId),
+                eq(MediaData.PLAYBACK_CAST_LOCAL)
+            )
 
         // update to remote cast
-        val rcn = SbnBuilder().run {
-            setPkg(SYSTEM_PACKAGE_NAME) // System package
-            modifyNotification(context).also {
-                it.setSmallIcon(android.R.drawable.ic_media_pause)
-                it.setStyle(MediaStyle().apply {
-                    setMediaSession(session.sessionToken)
-                    setRemotePlaybackInfo("Remote device", 0, null)
-                })
+        val rcn =
+            SbnBuilder().run {
+                setPkg(SYSTEM_PACKAGE_NAME) // System package
+                modifyNotification(context).also {
+                    it.setSmallIcon(android.R.drawable.ic_media_pause)
+                    it.setStyle(
+                        MediaStyle().apply {
+                            setMediaSession(session.sessionToken)
+                            setRemotePlaybackInfo("Remote device", 0, null)
+                        }
+                    )
+                }
+                build()
             }
-            build()
-        }
 
         mediaDataManager.onNotificationAdded(KEY, rcn)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(logger).logPlaybackLocationChange(anyInt(), eq(SYSTEM_PACKAGE_NAME),
-            eq(instanceId), eq(MediaData.PLAYBACK_CAST_REMOTE))
+        verify(logger)
+            .logPlaybackLocationChange(
+                anyInt(),
+                eq(SYSTEM_PACKAGE_NAME),
+                eq(instanceId),
+                eq(MediaData.PLAYBACK_CAST_REMOTE)
+            )
     }
 
     @Test
@@ -977,14 +1202,19 @@
         verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
 
         // Callback gets an updated state
-        val state = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 1f)
-                .build()
+        val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
         callbackCaptor.value.invoke(KEY, state)
 
         // Listener is notified of updated state
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
-                capture(mediaDataCaptor), eq(true), eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.isPlaying).isTrue()
     }
 
@@ -996,8 +1226,8 @@
         // No media added with this key
 
         callbackCaptor.value.invoke(KEY, state)
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(),
-                anyBoolean())
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
     @Test
@@ -1015,35 +1245,42 @@
 
         // Then no changes are made
         callbackCaptor.value.invoke(KEY, state)
-        verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(),
-            anyBoolean())
+        verify(listener, never())
+            .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean())
     }
 
     @Test
     fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() {
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
-        val state = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
-                .build()
+        val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build()
         whenever(controller.playbackState).thenReturn(state)
 
         addNotificationAndLoad()
         verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
         callbackCaptor.value.invoke(KEY, state)
 
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
-                capture(mediaDataCaptor), eq(true), eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.isPlaying).isFalse()
         assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
     }
 
     @Test
     fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() {
-        val desc = MediaDescription.Builder().run {
-            setTitle(SESSION_TITLE)
-            build()
-        }
-        val state = PlaybackState.Builder()
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        val state =
+            PlaybackState.Builder()
                 .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
                 .setActions(PlaybackState.ACTION_PLAY_PAUSE)
                 .build()
@@ -1051,13 +1288,13 @@
         // Add resumption controls in order to have semantic actions.
         // To make sure that they are not null after changing state.
         mediaDataManager.addResumptionControls(
-                USER_ID,
-                desc,
-                Runnable {},
-                session.sessionToken,
-                APP_NAME,
-                pendingIntent,
-                PACKAGE_NAME
+            USER_ID,
+            desc,
+            Runnable {},
+            session.sessionToken,
+            APP_NAME,
+            pendingIntent,
+            PACKAGE_NAME
         )
         backgroundExecutor.runAllReady()
         foregroundExecutor.runAllReady()
@@ -1066,14 +1303,14 @@
         callbackCaptor.value.invoke(PACKAGE_NAME, state)
 
         verify(listener)
-                .onMediaDataLoaded(
-                        eq(PACKAGE_NAME),
-                        eq(PACKAGE_NAME),
-                        capture(mediaDataCaptor),
-                        eq(true),
-                        eq(0),
-                        eq(false)
-                )
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(PACKAGE_NAME),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.isPlaying).isFalse()
         assertThat(mediaDataCaptor.value.semanticActions).isNotNull()
     }
@@ -1081,7 +1318,8 @@
     @Test
     fun testPlaybackStateNull_Pause_keyExists_callsListener() {
         whenever(controller.playbackState).thenReturn(null)
-        val state = PlaybackState.Builder()
+        val state =
+            PlaybackState.Builder()
                 .setState(PlaybackState.STATE_PAUSED, 0L, 1f)
                 .setActions(PlaybackState.ACTION_PLAY_PAUSE)
                 .build()
@@ -1090,20 +1328,32 @@
         verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor)
         callbackCaptor.value.invoke(KEY, state)
 
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY),
-                capture(mediaDataCaptor), eq(true), eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
         assertThat(mediaDataCaptor.value.isPlaying).isFalse()
         assertThat(mediaDataCaptor.value.semanticActions).isNull()
     }
 
-    /**
-     * Helper function to add a media notification and capture the resulting MediaData
-     */
+    /** Helper function to add a media notification and capture the resulting MediaData */
     private fun addNotificationAndLoad() {
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
-            eq(0), eq(false))
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
similarity index 93%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
index 121c894..a45e9d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.bluetooth.BluetoothLeBroadcast
 import android.bluetooth.BluetoothLeBroadcastMetadata
@@ -37,6 +37,10 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -109,7 +113,8 @@
         fakeFgExecutor = FakeExecutor(FakeSystemClock())
         fakeBgExecutor = FakeExecutor(FakeSystemClock())
         localBluetoothManager = mDependency.injectMockDependency(LocalBluetoothManager::class.java)
-        manager = MediaDeviceManager(
+        manager =
+            MediaDeviceManager(
                 context,
                 controllerFactory,
                 lmmFactory,
@@ -120,7 +125,7 @@
                 fakeFgExecutor,
                 fakeBgExecutor,
                 dumpster
-        )
+            )
         manager.addListener(listener)
 
         // Configure mocks.
@@ -134,11 +139,9 @@
         // Create a media sesssion and notification for testing.
         session = MediaSession(context, SESSION_KEY)
 
-        mediaData = MediaTestUtils.emptyMediaData.copy(
-                packageName = PACKAGE,
-                token = session.sessionToken)
-        whenever(controllerFactory.create(session.sessionToken))
-                .thenReturn(controller)
+        mediaData =
+            MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken)
+        whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller)
         setupLeAudioConfiguration(false)
     }
 
@@ -354,7 +357,9 @@
         val deviceCallback = captureCallback()
         // First set a non-null about-to-connect device
         deviceCallback.onAboutToConnectDeviceAdded(
-            "fakeAddress", "AboutToConnectDeviceName", mock(Drawable::class.java)
+            "fakeAddress",
+            "AboutToConnectDeviceName",
+            mock(Drawable::class.java)
         )
         // Run and reset the executors and listeners so we only focus on new events.
         fakeBgExecutor.runAllReady()
@@ -583,8 +588,8 @@
     @Test
     fun testRemotePlaybackDeviceOverride() {
         whenever(route.name).thenReturn(DEVICE_NAME)
-        val deviceData = MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null,
-                showBroadcastButton = false)
+        val deviceData =
+            MediaDeviceData(false, null, REMOTE_DEVICE_NAME, null, showBroadcastButton = false)
         val mediaDataWithDevice = mediaData.copy(device = deviceData)
 
         // GIVEN media data that already has a device set
@@ -613,8 +618,8 @@
         val data = captureDeviceData(KEY)
         assertThat(data.showBroadcastButton).isTrue()
         assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(context.getString(
-                R.string.broadcasting_description_is_broadcasting))
+        assertThat(data.name)
+            .isEqualTo(context.getString(R.string.broadcasting_description_is_broadcasting))
     }
 
     @Test
@@ -655,20 +660,21 @@
     }
 
     fun setupBroadcastCallback(): BluetoothLeBroadcast.Callback {
-        val callback: BluetoothLeBroadcast.Callback = object : BluetoothLeBroadcast.Callback {
-            override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastStartFailed(reason: Int) {}
-            override fun onBroadcastStopped(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastStopFailed(reason: Int) {}
-            override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
-            override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastMetadataChanged(
-                broadcastId: Int,
-                metadata: BluetoothLeBroadcastMetadata
-            ) {}
-        }
+        val callback: BluetoothLeBroadcast.Callback =
+            object : BluetoothLeBroadcast.Callback {
+                override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastStartFailed(reason: Int) {}
+                override fun onBroadcastStopped(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastStopFailed(reason: Int) {}
+                override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
+                override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
+                override fun onBroadcastMetadataChanged(
+                    broadcastId: Int,
+                    metadata: BluetoothLeBroadcastMetadata
+                ) {}
+            }
 
         bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback)
         return callback
@@ -677,7 +683,7 @@
     fun setupLeAudioConfiguration(isLeAudio: Boolean) {
         whenever(localBluetoothManager.profileManager).thenReturn(localBluetoothProfileManager)
         whenever(localBluetoothProfileManager.leAudioBroadcastProfile)
-                .thenReturn(localBluetoothLeBroadcast)
+            .thenReturn(localBluetoothLeBroadcast)
         whenever(localBluetoothLeBroadcast.isEnabled(any())).thenReturn(isLeAudio)
         whenever(localBluetoothLeBroadcast.appSourceName).thenReturn(BROADCAST_APP_NAME)
     }
@@ -685,7 +691,7 @@
     fun setupBroadcastPackage(currentName: String) {
         whenever(lmm.packageName).thenReturn(PACKAGE)
         whenever(packageManager.getApplicationInfo(eq(PACKAGE), anyInt()))
-                .thenReturn(applicationInfo)
+            .thenReturn(applicationInfo)
         whenever(packageManager.getApplicationLabel(applicationInfo)).thenReturn(currentName)
         context.setMockPackageManager(packageManager)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt
similarity index 84%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt
index 5586453..3099609 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaSessionBasedFilterTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.media.session.MediaController
 import android.media.session.MediaController.PlaybackInfo
@@ -23,12 +23,12 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
-
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
-
 import org.junit.After
 import org.junit.Before
 import org.junit.Rule
@@ -42,17 +42,15 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 private const val PACKAGE = "PKG"
 private const val KEY = "TEST_KEY"
 private const val NOTIF_KEY = "TEST_KEY"
 
-private val info = MediaTestUtils.emptyMediaData.copy(
-    packageName = PACKAGE,
-    notificationKey = NOTIF_KEY
-)
+private val info =
+    MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY)
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -139,10 +137,10 @@
 
         // Capture listener
         bgExecutor.runAllReady()
-        val listenerCaptor = ArgumentCaptor.forClass(
-                MediaSessionManager.OnActiveSessionsChangedListener::class.java)
-        verify(mediaSessionManager).addOnActiveSessionsChangedListener(
-                listenerCaptor.capture(), any())
+        val listenerCaptor =
+            ArgumentCaptor.forClass(MediaSessionManager.OnActiveSessionsChangedListener::class.java)
+        verify(mediaSessionManager)
+            .addOnActiveSessionsChangedListener(listenerCaptor.capture(), any())
         sessionListener = listenerCaptor.value
 
         filter.addListener(mediaListener)
@@ -161,8 +159,8 @@
         filter.onMediaDataLoaded(KEY, null, mediaData1)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -184,8 +182,8 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -214,8 +212,8 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -230,15 +228,22 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
         // WHEN a loaded event is received that matches the local session
         filter.onMediaDataLoaded(KEY, null, mediaData2)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is filtered
-        verify(mediaListener, never()).onMediaDataLoaded(
-            eq(KEY), eq(null), eq(mediaData2), anyBoolean(), anyInt(), anyBoolean())
+        verify(mediaListener, never())
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(null),
+                eq(mediaData2),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean()
+            )
     }
 
     @Test
@@ -254,8 +259,8 @@
         fgExecutor.runAllReady()
         // THEN the event is not filtered because there isn't a notification for the remote
         // session.
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -272,16 +277,22 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
         // WHEN a loaded event is received that matches the local session
         filter.onMediaDataLoaded(key2, null, mediaData2)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is filtered
         verify(mediaListener, never())
-            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(),
-                    anyInt(), anyBoolean())
+            .onMediaDataLoaded(
+                eq(key2),
+                eq(null),
+                eq(mediaData2),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean()
+            )
         // AND there should be a removed event for key2
         verify(mediaListener).onMediaDataRemoved(eq(key2))
     }
@@ -300,15 +311,15 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
         // WHEN a loaded event is received that matches the remote session
         filter.onMediaDataLoaded(key2, null, mediaData2)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -324,15 +335,15 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
         // WHEN a loaded event is received that matches the local session
         filter.onMediaDataLoaded(KEY, null, mediaData2)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -350,8 +361,8 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the event is not filtered
-        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -373,8 +384,8 @@
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the key migration event is fired
-        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true), eq(0), eq(false))
     }
 
     @Test
@@ -404,14 +415,20 @@
         fgExecutor.runAllReady()
         // THEN the key migration event is filtered
         verify(mediaListener, never())
-            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean(),
-                    anyInt(), anyBoolean())
+            .onMediaDataLoaded(
+                eq(key2),
+                eq(null),
+                eq(mediaData2),
+                anyBoolean(),
+                anyInt(),
+                anyBoolean()
+            )
         // WHEN a loaded event is received that matches the remote session
         filter.onMediaDataLoaded(key2, null, mediaData1)
         bgExecutor.runAllReady()
         fgExecutor.runAllReady()
         // THEN the key migration event is fired
-        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true),
-                eq(0), eq(false))
+        verify(mediaListener)
+            .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true), eq(0), eq(false))
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
index 823d4ae..344dffa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListenerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.pipeline
 
 import android.media.MediaMetadata
 import android.media.session.MediaController
@@ -23,6 +23,9 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -41,11 +44,11 @@
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "KEY"
@@ -70,7 +73,8 @@
     @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
     @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
     @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
-    @Captor private lateinit var dozingCallbackCaptor:
+    @Captor
+    private lateinit var dozingCallbackCaptor:
         ArgumentCaptor<StatusBarStateController.StateListener>
     @JvmField @Rule val mockito = MockitoJUnit.rule()
     private lateinit var metadataBuilder: MediaMetadata.Builder
@@ -85,36 +89,41 @@
     fun setup() {
         `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
         executor = FakeExecutor(clock)
-        mediaTimeoutListener = MediaTimeoutListener(
-            mediaControllerFactory,
-            executor,
-            logger,
-            statusBarStateController,
-            clock
-        )
+        mediaTimeoutListener =
+            MediaTimeoutListener(
+                mediaControllerFactory,
+                executor,
+                logger,
+                statusBarStateController,
+                clock
+            )
         mediaTimeoutListener.timeoutCallback = timeoutCallback
         mediaTimeoutListener.stateCallback = stateCallback
 
         // Create a media session and notification for testing.
-        metadataBuilder = MediaMetadata.Builder().apply {
-            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
-            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
-        }
-        playbackBuilder = PlaybackState.Builder().apply {
-            setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
-            setActions(PlaybackState.ACTION_PLAY)
-        }
-        session = MediaSession(context, SESSION_KEY).apply {
-            setMetadata(metadataBuilder.build())
-            setPlaybackState(playbackBuilder.build())
-        }
+        metadataBuilder =
+            MediaMetadata.Builder().apply {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+            }
+        playbackBuilder =
+            PlaybackState.Builder().apply {
+                setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
+                setActions(PlaybackState.ACTION_PLAY)
+            }
+        session =
+            MediaSession(context, SESSION_KEY).apply {
+                setMetadata(metadataBuilder.build())
+                setPlaybackState(playbackBuilder.build())
+            }
         session.setActive(true)
 
-        mediaData = MediaTestUtils.emptyMediaData.copy(
-            app = PACKAGE,
-            packageName = PACKAGE,
-            token = session.sessionToken
-        )
+        mediaData =
+            MediaTestUtils.emptyMediaData.copy(
+                app = PACKAGE,
+                packageName = PACKAGE,
+                token = session.sessionToken
+            )
 
         resumeData = mediaData.copy(token = null, active = false, resumption = true)
     }
@@ -212,8 +221,9 @@
         // Assuming we're registered
         testOnMediaDataLoaded_registersPlaybackListener()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        )
         assertThat(executor.numPending()).isEqualTo(1)
         assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
     }
@@ -223,8 +233,9 @@
         // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 0f).build())
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
+        )
         assertThat(executor.numPending()).isEqualTo(0)
         verify(logger).logTimeoutCancelled(eq(KEY), any())
     }
@@ -234,8 +245,9 @@
         // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-                .setState(PlaybackState.STATE_STOPPED, 0L, 0f).build())
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
+        )
         assertThat(executor.numPending()).isEqualTo(1)
     }
 
@@ -329,9 +341,8 @@
     @Test
     fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() {
         // WHEN regular media is paused
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         `when`(mediaController.playbackState).thenReturn(pausedState)
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         assertThat(executor.numPending()).isEqualTo(1)
@@ -362,9 +373,8 @@
         mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
 
         // AND that media is resumed
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         `when`(mediaController.playbackState).thenReturn(playingState)
         mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
 
@@ -386,15 +396,11 @@
     @Test
     fun testOnMediaDataLoaded_playbackActionsChanged_noCallback() {
         // Load media data once
-        val pausedState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .build()
+        val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
         loadMediaDataWithPlaybackState(pausedState)
 
         // When media data is loaded again, with different actions
-        val playingState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PLAY)
-                .build()
+        val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
         loadMediaDataWithPlaybackState(playingState)
 
         // Then the callback is not invoked
@@ -404,15 +410,11 @@
     @Test
     fun testOnPlaybackStateChanged_playbackActionsChanged_sendsCallback() {
         // Load media data once
-        val pausedState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PAUSE)
-                .build()
+        val pausedState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PAUSE).build()
         loadMediaDataWithPlaybackState(pausedState)
 
         // When the playback state changes, and has different actions
-        val playingState = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PLAY)
-                .build()
+        val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
 
         // Then the callback is invoked
@@ -421,24 +423,30 @@
 
     @Test
     fun testOnPlaybackStateChanged_differentCustomActions_sendsCallback() {
-        val customOne = PlaybackState.CustomAction.Builder(
+        val customOne =
+            PlaybackState.CustomAction.Builder(
                     "ACTION_1",
                     "custom action 1",
-                    android.R.drawable.ic_media_ff)
+                    android.R.drawable.ic_media_ff
+                )
                 .build()
-        val pausedState = PlaybackState.Builder()
+        val pausedState =
+            PlaybackState.Builder()
                 .setActions(PlaybackState.ACTION_PAUSE)
                 .addCustomAction(customOne)
                 .build()
         loadMediaDataWithPlaybackState(pausedState)
 
         // When the playback state actions change
-        val customTwo = PlaybackState.CustomAction.Builder(
-                "ACTION_2",
-                "custom action 2",
-                android.R.drawable.ic_media_rew)
+        val customTwo =
+            PlaybackState.CustomAction.Builder(
+                    "ACTION_2",
+                    "custom action 2",
+                    android.R.drawable.ic_media_rew
+                )
                 .build()
-        val pausedStateTwoActions = PlaybackState.Builder()
+        val pausedStateTwoActions =
+            PlaybackState.Builder()
                 .setActions(PlaybackState.ACTION_PAUSE)
                 .addCustomAction(customOne)
                 .addCustomAction(customTwo)
@@ -451,9 +459,7 @@
 
     @Test
     fun testOnPlaybackStateChanged_sameActions_noCallback() {
-        val stateWithActions = PlaybackState.Builder()
-                .setActions(PlaybackState.ACTION_PLAY)
-                .build()
+        val stateWithActions = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
         loadMediaDataWithPlaybackState(stateWithActions)
 
         // When the playback state updates with the same actions
@@ -467,18 +473,20 @@
     fun testOnPlaybackStateChanged_sameCustomActions_noCallback() {
         val actionName = "custom action"
         val actionIcon = android.R.drawable.ic_media_ff
-        val customOne = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
-                .build()
-        val stateOne = PlaybackState.Builder()
+        val customOne =
+            PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
+        val stateOne =
+            PlaybackState.Builder()
                 .setActions(PlaybackState.ACTION_PAUSE)
                 .addCustomAction(customOne)
                 .build()
         loadMediaDataWithPlaybackState(stateOne)
 
         // When the playback state is updated, but has the same actions
-        val customTwo = PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon)
-                .build()
-        val stateTwo = PlaybackState.Builder()
+        val customTwo =
+            PlaybackState.CustomAction.Builder(actionName, actionName, actionIcon).build()
+        val stateTwo =
+            PlaybackState.Builder()
                 .setActions(PlaybackState.ACTION_PAUSE)
                 .addCustomAction(customTwo)
                 .build()
@@ -491,15 +499,13 @@
     @Test
     fun testOnMediaDataLoaded_isPlayingChanged_noCallback() {
         // Load media data in paused state
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         loadMediaDataWithPlaybackState(pausedState)
 
         // When media data is loaded again but playing
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 1f)
-                .build()
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
         loadMediaDataWithPlaybackState(playingState)
 
         // Then the callback is not invoked
@@ -509,15 +515,13 @@
     @Test
     fun testOnPlaybackStateChanged_isPlayingChanged_sendsCallback() {
         // Load media data in paused state
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         loadMediaDataWithPlaybackState(pausedState)
 
         // When the playback state changes to playing
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PLAYING, 0L, 1f)
-                .build()
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
 
         // Then the callback is invoked
@@ -527,15 +531,13 @@
     @Test
     fun testOnPlaybackStateChanged_isPlayingSame_noCallback() {
         // Load media data in paused state
-        val pausedState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_PAUSED, 0L, 0f)
-                .build()
+        val pausedState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         loadMediaDataWithPlaybackState(pausedState)
 
         // When the playback state is updated, but still not playing
-        val playingState = PlaybackState.Builder()
-                .setState(PlaybackState.STATE_STOPPED, 0L, 0f)
-                .build()
+        val playingState =
+            PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
 
         // Then the callback is not invoked
@@ -546,8 +548,9 @@
     fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
         // When paused media is loaded
         testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-            .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        )
         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
 
         // And we doze past the scheduled timeout
@@ -571,8 +574,9 @@
         val time = clock.currentTimeMillis()
         clock.setElapsedRealtime(time)
         testOnMediaDataLoaded_registersPlaybackListener()
-        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
-            .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
+        mediaCallbackCaptor.value.onPlaybackStateChanged(
+            PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
+        )
         verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
 
         // And we doze, but not past the scheduled timeout
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
similarity index 78%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
index 83168cb..84fdfd7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/MediaResumeListenerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.resume
 
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -33,11 +33,16 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.pipeline.RESUME_MEDIA_TIMEOUT
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.time.FakeSystemClock
-import org.junit.After
 import com.google.common.truth.Truth.assertThat
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,7 +68,9 @@
 private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
 
 private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
 private fun <T> any(): T = Mockito.any<T>()
 
 @SmallTest
@@ -93,26 +100,32 @@
     private lateinit var resumeListener: MediaResumeListener
     private val clock = FakeSystemClock()
 
-    private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
-        Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
-    private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver,
-        Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+    private var originalQsSetting =
+        Settings.Global.getInt(
+            context.contentResolver,
+            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+            1
+        )
+    private var originalResumeSetting =
+        Settings.Secure.getInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
-        Settings.Global.putInt(context.contentResolver,
-            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
-        Settings.Secure.putInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
+        Settings.Global.putInt(
+            context.contentResolver,
+            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+            1
+        )
+        Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
 
         whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
-                .thenReturn(resumeBrowser)
+            .thenReturn(resumeBrowser)
 
         // resume components are stored in sharedpreferences
         whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
-                .thenReturn(sharedPrefs)
+            .thenReturn(sharedPrefs)
         whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
         whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
         whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
@@ -120,36 +133,59 @@
         whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
 
         executor = FakeExecutor(clock)
-        resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
+        resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
 
-        data = MediaTestUtils.emptyMediaData.copy(
+        data =
+            MediaTestUtils.emptyMediaData.copy(
                 song = TITLE,
                 packageName = PACKAGE_NAME,
-                token = token)
+                token = token
+            )
     }
 
     @After
     fun tearDown() {
-        Settings.Global.putInt(context.contentResolver,
-            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting)
-        Settings.Secure.putInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting)
+        Settings.Global.putInt(
+            context.contentResolver,
+            Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS,
+            originalQsSetting
+        )
+        Settings.Secure.putInt(
+            context.contentResolver,
+            Settings.Secure.MEDIA_CONTROLS_RESUME,
+            originalResumeSetting
+        )
     }
 
     @Test
     fun testWhenNoResumption_doesNothing() {
-        Settings.Secure.putInt(context.contentResolver,
-            Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+        Settings.Secure.putInt(context.contentResolver, Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
 
         // When listener is created, we do NOT register a user change listener
-        val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
-                resumeBrowserFactory, dumpManager, clock)
+        val listener =
+            MediaResumeListener(
+                context,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
         listener.setManager(mediaDataManager)
-        verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
-            any(), any(), any(), anyInt(), any())
+        verify(broadcastDispatcher, never())
+            .registerReceiver(eq(listener.userChangeReceiver), any(), any(), any(), anyInt(), any())
 
         // When data is loaded, we do NOT execute or update anything
         listener.onMediaDataLoaded(KEY, OLD_KEY, data)
@@ -170,9 +206,7 @@
     fun testOnLoad_checksForResume_badService() {
         setUpMbsWithValidResolveInfo()
 
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.onError()
-        }
+        whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
 
         // When media data is loaded that has not been checked yet, and does not have a MBS
         resumeListener.onMediaDataLoaded(KEY, null, data)
@@ -226,7 +260,7 @@
 
         // But we do not tell it to add new controls
         verify(mediaDataManager, never())
-                .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
     }
 
     @Test
@@ -253,8 +287,15 @@
 
         // Make sure broadcast receiver is registered
         resumeListener.setManager(mediaDataManager)
-        verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver),
-                any(), any(), any(), anyInt(), any())
+        verify(broadcastDispatcher)
+            .registerReceiver(
+                eq(resumeListener.userChangeReceiver),
+                any(),
+                any(),
+                any(),
+                anyInt(),
+                any()
+            )
 
         // When we get an unlock event
         val intent = Intent(Intent.ACTION_USER_UNLOCKED)
@@ -264,8 +305,8 @@
         verify(resumeBrowser, times(3)).findRecentMedia()
 
         // Then since the mock service found media, the manager should be informed
-        verify(mediaDataManager, times(3)).addResumptionControls(anyInt(),
-                any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
+        verify(mediaDataManager, times(3))
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
     }
 
     @Test
@@ -304,12 +345,14 @@
 
         // Then we save an update with the current time
         verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
-        componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
-                .dropLastWhile { it.isEmpty() }.forEach {
-            val result = it.split("/")
-            assertThat(result.size).isEqualTo(3)
-            assertThat(result[2].toLong()).isEqualTo(currentTime)
-        }
+        componentCaptor.value
+            .split(ResumeMediaBrowser.DELIMITER.toRegex())
+            .dropLastWhile { it.isEmpty() }
+            .forEach {
+                val result = it.split("/")
+                assertThat(result.size).isEqualTo(3)
+                assertThat(result[2].toLong()).isEqualTo(currentTime)
+            }
         verify(sharedPrefsEditor, times(1)).apply()
     }
 
@@ -328,8 +371,16 @@
         val lastPlayed = clock.currentTimeMillis()
         val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
         whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
-        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
+        val resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
 
@@ -339,8 +390,8 @@
 
         // We add its resume controls
         verify(resumeBrowser, times(1)).findRecentMedia()
-        verify(mediaDataManager, times(1)).addResumptionControls(anyInt(),
-                any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
+        verify(mediaDataManager, times(1))
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
     }
 
     @Test
@@ -349,8 +400,16 @@
         val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100
         val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
         whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
-        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
+        val resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
 
@@ -360,8 +419,8 @@
 
         // We do not try to add resume controls
         verify(resumeBrowser, times(0)).findRecentMedia()
-        verify(mediaDataManager, times(0)).addResumptionControls(anyInt(),
-                any(), any(), any(), any(), any(), any())
+        verify(mediaDataManager, times(0))
+            .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
     }
 
     @Test
@@ -380,8 +439,16 @@
         val lastPlayed = currentTime - 1000
         val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:"
         whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString)
-        val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
-                tunerService, resumeBrowserFactory, dumpManager, clock)
+        val resumeListener =
+            MediaResumeListener(
+                mockContext,
+                broadcastDispatcher,
+                executor,
+                tunerService,
+                resumeBrowserFactory,
+                dumpManager,
+                clock
+            )
         resumeListener.setManager(mediaDataManager)
         mediaDataManager.addListener(resumeListener)
 
@@ -391,12 +458,14 @@
 
         // Then we store the new lastPlayed time
         verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
-        componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex())
-                .dropLastWhile { it.isEmpty() }.forEach {
-                    val result = it.split("/")
-                    assertThat(result.size).isEqualTo(3)
-                    assertThat(result[2].toLong()).isEqualTo(currentTime)
-                }
+        componentCaptor.value
+            .split(ResumeMediaBrowser.DELIMITER.toRegex())
+            .dropLastWhile { it.isEmpty() }
+            .forEach {
+                val result = it.split("/")
+                assertThat(result.size).isEqualTo(3)
+                assertThat(result[2].toLong()).isEqualTo(currentTime)
+            }
         verify(sharedPrefsEditor, times(1)).apply()
     }
 
@@ -417,9 +486,7 @@
         setUpMbsWithValidResolveInfo()
 
         // Set up mocks to return with an error
-        whenever(resumeBrowser.testConnection()).thenAnswer {
-            callbackCaptor.value.onError()
-        }
+        whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
 
         resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
         executor.runAllReady()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
similarity index 91%
rename from packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
index dafaa6b..a04cfd4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/resume/ResumeMediaBrowserTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.resume
 
 import android.content.ComponentName
 import android.content.Context
@@ -37,8 +37,8 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 private const val PACKAGE_NAME = "package"
 private const val CLASS_NAME = "class"
@@ -47,7 +47,9 @@
 private const val ROOT = "media browser root"
 
 private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
 private fun <T> any(): T = Mockito.any<T>()
 
 @SmallTest
@@ -57,10 +59,8 @@
 
     private lateinit var resumeBrowser: TestableResumeMediaBrowser
     private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
-    private val description = MediaDescription.Builder()
-            .setTitle(TITLE)
-            .setMediaId(MEDIA_ID)
-            .build()
+    private val description =
+        MediaDescription.Builder().setTitle(TITLE).setMediaId(MEDIA_ID).build()
 
     @Mock lateinit var callback: ResumeMediaBrowser.Callback
     @Mock lateinit var listener: MediaResumeListener
@@ -81,19 +81,20 @@
         MockitoAnnotations.initMocks(this)
 
         whenever(browserFactory.create(any(), capture(connectionCallback), any()))
-                .thenReturn(browser)
+            .thenReturn(browser)
 
         whenever(mediaController.transportControls).thenReturn(transportControls)
         whenever(mediaController.sessionToken).thenReturn(token)
 
-        resumeBrowser = TestableResumeMediaBrowser(
-            context,
-            callback,
-            component,
-            browserFactory,
-            logger,
-            mediaController
-        )
+        resumeBrowser =
+            TestableResumeMediaBrowser(
+                context,
+                callback,
+                component,
+                browserFactory,
+                logger,
+                mediaController
+            )
     }
 
     @Test
@@ -329,30 +330,20 @@
         verify(oldBrowser).disconnect()
     }
 
-    /**
-     * Helper function to mock a failed connection
-     */
+    /** Helper function to mock a failed connection */
     private fun setupBrowserFailed() {
-        whenever(browser.connect()).thenAnswer {
-            connectionCallback.value.onConnectionFailed()
-        }
+        whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnectionFailed() }
     }
 
-    /**
-     * Helper function to mock a successful connection only
-     */
+    /** Helper function to mock a successful connection only */
     private fun setupBrowserConnection() {
-        whenever(browser.connect()).thenAnswer {
-            connectionCallback.value.onConnected()
-        }
+        whenever(browser.connect()).thenAnswer { connectionCallback.value.onConnected() }
         whenever(browser.isConnected()).thenReturn(true)
         whenever(browser.getRoot()).thenReturn(ROOT)
         whenever(browser.sessionToken).thenReturn(token)
     }
 
-    /**
-     * Helper function to mock a successful connection, but no media results
-     */
+    /** Helper function to mock a successful connection, but no media results */
     private fun setupBrowserConnectionNoResults() {
         setupBrowserConnection()
         whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
@@ -360,9 +351,7 @@
         }
     }
 
-    /**
-     * Helper function to mock a successful connection, but no playable results
-     */
+    /** Helper function to mock a successful connection, but no playable results */
     private fun setupBrowserConnectionNotPlayable() {
         setupBrowserConnection()
 
@@ -373,9 +362,7 @@
         }
     }
 
-    /**
-     * Helper function to mock a successful connection with playable media
-     */
+    /** Helper function to mock a successful connection with playable media */
     private fun setupBrowserConnectionValidMedia() {
         setupBrowserConnection()
 
@@ -387,9 +374,7 @@
         }
     }
 
-    /**
-     * Override so media controller use is testable
-     */
+    /** Override so media controller use is testable */
     private class TestableResumeMediaBrowser(
         context: Context,
         callback: Callback,
@@ -403,4 +388,4 @@
             return fakeController
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt
index e4cab18..99f56b1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/AnimationBindHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/AnimationBindHandlerTest.kt
@@ -14,26 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
-import org.mockito.Mockito.`when` as whenever
 import android.graphics.drawable.Animatable2
 import android.graphics.drawable.Drawable
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import com.android.systemui.SysuiTestCase
-import junit.framework.Assert.assertTrue
 import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
 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.Mockito.verify
-import org.mockito.Mockito.times
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
@@ -56,8 +56,7 @@
         handler = AnimationBindHandler()
     }
 
-    @After
-    fun tearDown() {}
+    @After fun tearDown() {}
 
     @Test
     fun registerNoAnimations_executeCallbackImmediately() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt
similarity index 90%
rename from packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt
index f56d42e..5bb74e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/ColorSchemeTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.animation.ValueAnimator
 import android.graphics.Color
@@ -22,6 +22,8 @@
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.models.player.MediaViewHolder
 import com.android.systemui.monet.ColorScheme
 import junit.framework.Assert.assertEquals
 import org.junit.After
@@ -67,21 +69,18 @@
         animatingColorTransitionFactory = { _, _, _ -> mockAnimatingTransition }
         whenever(extractColor.invoke(colorScheme)).thenReturn(TARGET_COLOR)
 
-        colorSchemeTransition = ColorSchemeTransition(
-            context, mediaViewHolder, animatingColorTransitionFactory
-        )
+        colorSchemeTransition =
+            ColorSchemeTransition(context, mediaViewHolder, animatingColorTransitionFactory)
 
-        colorTransition = object : AnimatingColorTransition(
-            DEFAULT_COLOR, extractColor, applyColor
-        ) {
-            override fun buildAnimator(): ValueAnimator {
-                return valueAnimator
+        colorTransition =
+            object : AnimatingColorTransition(DEFAULT_COLOR, extractColor, applyColor) {
+                override fun buildAnimator(): ValueAnimator {
+                    return valueAnimator
+                }
             }
-        }
     }
 
-    @After
-    fun tearDown() {}
+    @After fun tearDown() {}
 
     @Test
     fun testColorTransition_nullColorScheme_keepsDefault() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt
similarity index 84%
rename from packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt
index c41fac7..2026006 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/KeyguardMediaControllerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.provider.Settings
 import android.test.suitebuilder.annotation.SmallTest
@@ -48,17 +48,12 @@
 @TestableLooper.RunWithLooper
 class KeyguardMediaControllerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var mediaHost: MediaHost
-    @Mock
-    private lateinit var bypassController: KeyguardBypassController
-    @Mock
-    private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock
-    private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var mediaHost: MediaHost
+    @Mock private lateinit var bypassController: KeyguardBypassController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var configurationController: ConfigurationController
 
-    @JvmField @Rule
-    val mockito = MockitoJUnit.rule()
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
 
     private val mediaContainerView: MediaContainerView = MediaContainerView(context, null)
     private val hostView = UniqueObjectHostView(context)
@@ -76,15 +71,16 @@
         hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
         testableLooper = TestableLooper.get(this)
         fakeHandler = FakeHandler(testableLooper.looper)
-        keyguardMediaController = KeyguardMediaController(
-            mediaHost,
-            bypassController,
-            statusBarStateController,
-            context,
-            settings,
-            fakeHandler,
-            configurationController,
-        )
+        keyguardMediaController =
+            KeyguardMediaController(
+                mediaHost,
+                bypassController,
+                statusBarStateController,
+                context,
+                settings,
+                fakeHandler,
+                configurationController,
+            )
         keyguardMediaController.attachSinglePaneContainer(mediaContainerView)
         keyguardMediaController.useSplitShade = false
     }
@@ -153,8 +149,10 @@
         keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
         keyguardMediaController.useSplitShade = true
 
-        assertTrue("HostView wasn't attached to the split pane container",
-            splitShadeContainer.childCount == 1)
+        assertTrue(
+            "HostView wasn't attached to the split pane container",
+            splitShadeContainer.childCount == 1
+        )
     }
 
     @Test
@@ -162,8 +160,10 @@
         val splitShadeContainer = FrameLayout(context)
         keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
 
-        assertTrue("HostView wasn't attached to the single pane container",
-            mediaContainerView.childCount == 1)
+        assertTrue(
+            "HostView wasn't attached to the single pane container",
+            mediaContainerView.childCount == 1
+        )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
new file mode 100644
index 0000000..c8e8943
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.app.PendingIntent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.InstanceId
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.PAGINATION_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.controls.ui.MediaHierarchyManager.Companion.LOCATION_QS
+import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
+import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import javax.inject.Provider
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+private val DATA = MediaTestUtils.emptyMediaData
+
+private val SMARTSPACE_KEY = "smartspace"
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaCarouselControllerTest : SysuiTestCase() {
+
+    @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
+    @Mock lateinit var panel: MediaControlPanel
+    @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
+    @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
+    @Mock lateinit var mediaHostState: MediaHostState
+    @Mock lateinit var activityStarter: ActivityStarter
+    @Mock @Main private lateinit var executor: DelayableExecutor
+    @Mock lateinit var mediaDataManager: MediaDataManager
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var falsingManager: FalsingManager
+    @Mock lateinit var dumpManager: DumpManager
+    @Mock lateinit var logger: MediaUiEventLogger
+    @Mock lateinit var debugLogger: MediaCarouselControllerLogger
+    @Mock lateinit var mediaViewController: MediaViewController
+    @Mock lateinit var smartspaceMediaData: SmartspaceMediaData
+    @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
+    @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
+
+    private val clock = FakeSystemClock()
+    private lateinit var mediaCarouselController: MediaCarouselController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mediaCarouselController =
+            MediaCarouselController(
+                context,
+                mediaControlPanelFactory,
+                visualStabilityProvider,
+                mediaHostStatesManager,
+                activityStarter,
+                clock,
+                executor,
+                mediaDataManager,
+                configurationController,
+                falsingCollector,
+                falsingManager,
+                dumpManager,
+                logger,
+                debugLogger
+            )
+        verify(mediaDataManager).addListener(capture(listener))
+        verify(visualStabilityProvider)
+            .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
+        whenever(mediaControlPanelFactory.get()).thenReturn(panel)
+        whenever(panel.mediaViewController).thenReturn(mediaViewController)
+        whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
+        MediaPlayerData.clear()
+    }
+
+    @Test
+    fun testPlayerOrdering() {
+        // Test values: key, data, last active time
+        val playingLocal =
+            Triple(
+                "playing local",
+                DATA.copy(
+                    active = true,
+                    isPlaying = true,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = false
+                ),
+                4500L
+            )
+
+        val playingCast =
+            Triple(
+                "playing cast",
+                DATA.copy(
+                    active = true,
+                    isPlaying = true,
+                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
+                    resumption = false
+                ),
+                5000L
+            )
+
+        val pausedLocal =
+            Triple(
+                "paused local",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = false
+                ),
+                1000L
+            )
+
+        val pausedCast =
+            Triple(
+                "paused cast",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_CAST_LOCAL,
+                    resumption = false
+                ),
+                2000L
+            )
+
+        val playingRcn =
+            Triple(
+                "playing RCN",
+                DATA.copy(
+                    active = true,
+                    isPlaying = true,
+                    playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
+                    resumption = false
+                ),
+                5000L
+            )
+
+        val pausedRcn =
+            Triple(
+                "paused RCN",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_CAST_REMOTE,
+                    resumption = false
+                ),
+                5000L
+            )
+
+        val active =
+            Triple(
+                "active",
+                DATA.copy(
+                    active = true,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true
+                ),
+                250L
+            )
+
+        val resume1 =
+            Triple(
+                "resume 1",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true
+                ),
+                500L
+            )
+
+        val resume2 =
+            Triple(
+                "resume 2",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true
+                ),
+                1000L
+            )
+
+        val activeMoreRecent =
+            Triple(
+                "active more recent",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true,
+                    lastActive = 2L
+                ),
+                1000L
+            )
+
+        val activeLessRecent =
+            Triple(
+                "active less recent",
+                DATA.copy(
+                    active = false,
+                    isPlaying = false,
+                    playbackLocation = MediaData.PLAYBACK_LOCAL,
+                    resumption = true,
+                    lastActive = 1L
+                ),
+                1000L
+            )
+        // Expected ordering for media players:
+        // Actively playing local sessions
+        // Actively playing cast sessions
+        // Paused local and cast sessions, by last active
+        // RCNs
+        // Resume controls, by last active
+
+        val expected =
+            listOf(
+                playingLocal,
+                playingCast,
+                pausedCast,
+                pausedLocal,
+                playingRcn,
+                pausedRcn,
+                active,
+                resume2,
+                resume1
+            )
+
+        expected.forEach {
+            clock.setCurrentTimeMillis(it.third)
+            MediaPlayerData.addMediaPlayer(
+                it.first,
+                it.second.copy(notificationKey = it.first),
+                panel,
+                clock,
+                isSsReactivated = false
+            )
+        }
+
+        for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
+            assertEquals(expected.get(index).first, key.data.notificationKey)
+        }
+
+        for ((index, key) in MediaPlayerData.visiblePlayerKeys().withIndex()) {
+            assertEquals(expected.get(index).first, key.data.notificationKey)
+        }
+    }
+
+    @Test
+    fun testOrderWithSmartspace_prioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is prioritized
+        MediaPlayerData.addMediaRecommendation(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            panel,
+            true,
+            clock
+        )
+
+        // Then it should be shown immediately after any actively playing controls
+        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
+    }
+
+    @Test
+    fun testOrderWithSmartspace_prioritized_updatingVisibleMediaPlayers() {
+        testPlayerOrdering()
+
+        // If smartspace is prioritized
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
+            true
+        )
+
+        // Then it should be shown immediately after any actively playing controls
+        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
+        assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(2).isSsMediaRec)
+    }
+
+    @Test
+    fun testOrderWithSmartspace_notPrioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is not prioritized
+        MediaPlayerData.addMediaRecommendation(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            panel,
+            false,
+            clock
+        )
+
+        // Then it should be shown at the end of the carousel's active entries
+        val idx = MediaPlayerData.playerKeys().count { it.data.active } - 1
+        assertTrue(MediaPlayerData.playerKeys().elementAt(idx).isSsMediaRec)
+    }
+
+    @Test
+    fun testPlayingExistingMediaPlayerFromCarousel_visibleMediaPlayersNotUpdated() {
+        testPlayerOrdering()
+        // playing paused player
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            "paused local",
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            "playing local",
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = true
+            )
+        )
+
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+        // paused player order should stays the same in visibleMediaPLayer map.
+        // paused player order should be first in mediaPlayer map.
+        assertEquals(
+            MediaPlayerData.visiblePlayerKeys().elementAt(3),
+            MediaPlayerData.playerKeys().elementAt(0)
+        )
+    }
+    @Test
+    fun testSwipeDismiss_logged() {
+        mediaCarouselController.mediaCarouselScrollHandler.dismissCallback.invoke()
+
+        verify(logger).logSwipeDismiss()
+    }
+
+    @Test
+    fun testSettingsButton_logged() {
+        mediaCarouselController.settingsButton.callOnClick()
+
+        verify(logger).logCarouselSettings()
+    }
+
+    @Test
+    fun testLocationChangeQs_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_QS,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
+    }
+
+    @Test
+    fun testLocationChangeQqs_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_QQS,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
+    }
+
+    @Test
+    fun testLocationChangeLockscreen_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_LOCKSCREEN,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
+    }
+
+    @Test
+    fun testLocationChangeDream_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
+            mediaHostState,
+            animate = false
+        )
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
+    }
+
+    @Test
+    fun testRecommendationRemoved_logged() {
+        val packageName = "smartspace package"
+        val instanceId = InstanceId.fakeInstanceId(123)
+
+        val smartspaceData =
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = packageName, instanceId = instanceId)
+        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, smartspaceData, panel, true, clock)
+        mediaCarouselController.removePlayer(SMARTSPACE_KEY)
+
+        verify(logger).logRecommendationRemoved(eq(packageName), eq(instanceId!!))
+    }
+
+    @Test
+    fun testMediaLoaded_ScrollToActivePlayer() {
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+        // adding a media recommendation card.
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA,
+            false
+        )
+        mediaCarouselController.shouldScrollToKey = true
+        // switching between media players.
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            "playing local",
+            DATA.copy(
+                active = true,
+                isPlaying = false,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = true
+            )
+        )
+        listener.value.onMediaDataLoaded(
+            "paused local",
+            "paused local",
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+
+        assertEquals(
+            MediaPlayerData.getMediaPlayerIndex("paused local"),
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+    }
+
+    @Test
+    fun testMediaLoadedFromRecommendationCard_ScrollToActivePlayer() {
+        listener.value.onSmartspaceMediaDataLoaded(
+            SMARTSPACE_KEY,
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(packageName = "PACKAGE_NAME", isActive = true),
+            false
+        )
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false
+            )
+        )
+
+        var playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
+        assertEquals(
+            playerIndex,
+            mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
+        )
+        assertEquals(playerIndex, 0)
+
+        // Replaying the same media player one more time.
+        // And check that the card stays in its position.
+        mediaCarouselController.shouldScrollToKey = true
+        listener.value.onMediaDataLoaded(
+            "playing local",
+            null,
+            DATA.copy(
+                active = true,
+                isPlaying = true,
+                playbackLocation = MediaData.PLAYBACK_LOCAL,
+                resumption = false,
+                packageName = "PACKAGE_NAME"
+            )
+        )
+        playerIndex = MediaPlayerData.getMediaPlayerIndex("playing local")
+        assertEquals(playerIndex, 0)
+    }
+
+    @Test
+    fun testRecommendationRemovedWhileNotVisible_updateHostVisibility() {
+        var result = false
+        mediaCarouselController.updateHostVisibility = { result = true }
+
+        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(true)
+        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
+
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun testRecommendationRemovedWhileVisible_thenReorders_updateHostVisibility() {
+        var result = false
+        mediaCarouselController.updateHostVisibility = { result = true }
+
+        whenever(visualStabilityProvider.isReorderingAllowed).thenReturn(false)
+        listener.value.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY, false)
+        assertEquals(false, result)
+
+        visualStabilityCallback.value.onReorderingAllowed()
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun testGetCurrentVisibleMediaContentIntent() {
+        val clickIntent1 = mock(PendingIntent::class.java)
+        val player1 = Triple("player1", DATA.copy(clickIntent = clickIntent1), 1000L)
+        clock.setCurrentTimeMillis(player1.third)
+        MediaPlayerData.addMediaPlayer(
+            player1.first,
+            player1.second.copy(notificationKey = player1.first),
+            panel,
+            clock,
+            isSsReactivated = false
+        )
+
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
+
+        val clickIntent2 = mock(PendingIntent::class.java)
+        val player2 = Triple("player2", DATA.copy(clickIntent = clickIntent2), 2000L)
+        clock.setCurrentTimeMillis(player2.third)
+        MediaPlayerData.addMediaPlayer(
+            player2.first,
+            player2.second.copy(notificationKey = player2.first),
+            panel,
+            clock,
+            isSsReactivated = false
+        )
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the front because it was active more recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+
+        val clickIntent3 = mock(PendingIntent::class.java)
+        val player3 = Triple("player3", DATA.copy(clickIntent = clickIntent3), 500L)
+        clock.setCurrentTimeMillis(player3.third)
+        MediaPlayerData.addMediaPlayer(
+            player3.first,
+            player3.second.copy(notificationKey = player3.first),
+            panel,
+            clock,
+            isSsReactivated = false
+        )
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the end because it was active less recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+    }
+
+    @Test
+    fun testSetCurrentState_UpdatePageIndicatorAlphaWhenSquish() {
+        val delta = 0.0001F
+        val paginationSquishMiddle =
+            TRANSFORM_BEZIER.getInterpolation(
+                (PAGINATION_DELAY + DURATION / 2) / ANIMATION_BASE_DURATION
+            )
+        val paginationSquishEnd =
+            TRANSFORM_BEZIER.getInterpolation(
+                (PAGINATION_DELAY + DURATION) / ANIMATION_BASE_DURATION
+            )
+        whenever(mediaHostStatesManager.mediaHostStates)
+            .thenReturn(mutableMapOf(LOCATION_QS to mediaHostState))
+        whenever(mediaHostState.visible).thenReturn(true)
+        mediaCarouselController.currentEndLocation = LOCATION_QS
+        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishMiddle)
+        mediaCarouselController.updatePageIndicatorAlpha()
+        assertEquals(mediaCarouselController.pageIndicator.alpha, 0.5F, delta)
+
+        whenever(mediaHostState.squishFraction).thenReturn(paginationSquishEnd)
+        mediaCarouselController.updatePageIndicatorAlpha()
+        assertEquals(mediaCarouselController.pageIndicator.alpha, 1.0F, delta)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
similarity index 80%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index 7de5719..5843053 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.animation.Animator
 import android.animation.AnimatorSet
@@ -59,7 +59,20 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bluetooth.BroadcastDialogController
 import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.media.MediaControlPanel.KEY_SMARTSPACE_APP_NAME
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.GutsViewHolder
+import com.android.systemui.media.controls.models.player.MediaAction
+import com.android.systemui.media.controls.models.player.MediaButton
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.media.controls.models.player.MediaDeviceData
+import com.android.systemui.media.controls.models.player.MediaViewHolder
+import com.android.systemui.media.controls.models.player.SeekBarViewModel
+import com.android.systemui.media.controls.models.recommendation.KEY_SMARTSPACE_APP_NAME
+import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
+import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
+import com.android.systemui.media.controls.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA
+import com.android.systemui.media.controls.pipeline.MediaDataManager
+import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.media.dialog.MediaOutputDialogFactory
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -164,8 +177,8 @@
 
     private lateinit var session: MediaSession
     private lateinit var device: MediaDeviceData
-    private val disabledDevice = MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null,
-            showBroadcastButton = false)
+    private val disabledDevice =
+        MediaDeviceData(false, null, DISABLED_DEVICE_NAME, null, showBroadcastButton = false)
     private lateinit var mediaData: MediaData
     private val clock = FakeSystemClock()
     @Mock private lateinit var logger: MediaUiEventLogger
@@ -212,24 +225,27 @@
         whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE)
         context.setMockPackageManager(packageManager)
 
-        player = object : MediaControlPanel(
-            context,
-            bgExecutor,
-            mainExecutor,
-            activityStarter,
-            broadcastSender,
-            mediaViewController,
-            seekBarViewModel,
-            Lazy { mediaDataManager },
-            mediaOutputDialogFactory,
-            mediaCarouselController,
-            falsingManager,
-            clock,
-            logger,
-            keyguardStateController,
-            activityIntentHelper,
-            lockscreenUserManager,
-            broadcastDialogController) {
+        player =
+            object :
+                MediaControlPanel(
+                    context,
+                    bgExecutor,
+                    mainExecutor,
+                    activityStarter,
+                    broadcastSender,
+                    mediaViewController,
+                    seekBarViewModel,
+                    Lazy { mediaDataManager },
+                    mediaOutputDialogFactory,
+                    mediaCarouselController,
+                    falsingManager,
+                    clock,
+                    logger,
+                    keyguardStateController,
+                    activityIntentHelper,
+                    lockscreenUserManager,
+                    broadcastDialogController
+                ) {
                 override fun loadAnimator(
                     animId: Int,
                     otionInterpolator: Interpolator,
@@ -250,18 +266,20 @@
         // Set valid recommendation data
         val extras = Bundle()
         extras.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME)
-        val intent = Intent().apply {
-            putExtras(extras)
-            setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        }
+        val intent =
+            Intent().apply {
+                putExtras(extras)
+                setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            }
         whenever(smartspaceAction.intent).thenReturn(intent)
         whenever(smartspaceAction.extras).thenReturn(extras)
-        smartspaceData = EMPTY_SMARTSPACE_MEDIA_DATA.copy(
-            packageName = PACKAGE,
-            instanceId = instanceId,
-            recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
-            cardAction = smartspaceAction
-        )
+        smartspaceData =
+            EMPTY_SMARTSPACE_MEDIA_DATA.copy(
+                packageName = PACKAGE,
+                instanceId = instanceId,
+                recommendations = listOf(smartspaceAction, smartspaceAction, smartspaceAction),
+                cardAction = smartspaceAction
+            )
     }
 
     private fun initGutsViewHolderMocks() {
@@ -279,36 +297,39 @@
     }
 
     private fun initDeviceMediaData(shouldShowBroadcastButton: Boolean, name: String) {
-        device = MediaDeviceData(true, null, name, null,
-                showBroadcastButton = shouldShowBroadcastButton)
+        device =
+            MediaDeviceData(true, null, name, null, showBroadcastButton = shouldShowBroadcastButton)
 
         // Create media session
-        val metadataBuilder = MediaMetadata.Builder().apply {
-            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
-            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
-        }
-        val playbackBuilder = PlaybackState.Builder().apply {
-            setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
-            setActions(PlaybackState.ACTION_PLAY)
-        }
-        session = MediaSession(context, SESSION_KEY).apply {
-            setMetadata(metadataBuilder.build())
-            setPlaybackState(playbackBuilder.build())
-        }
+        val metadataBuilder =
+            MediaMetadata.Builder().apply {
+                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+            }
+        val playbackBuilder =
+            PlaybackState.Builder().apply {
+                setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
+                setActions(PlaybackState.ACTION_PLAY)
+            }
+        session =
+            MediaSession(context, SESSION_KEY).apply {
+                setMetadata(metadataBuilder.build())
+                setPlaybackState(playbackBuilder.build())
+            }
         session.setActive(true)
 
-        mediaData = MediaTestUtils.emptyMediaData.copy(
+        mediaData =
+            MediaTestUtils.emptyMediaData.copy(
                 artist = ARTIST,
                 song = TITLE,
                 packageName = PACKAGE,
                 token = session.sessionToken,
                 device = device,
-                instanceId = instanceId)
+                instanceId = instanceId
+            )
     }
 
-    /**
-     * Initialize elements in media view holder
-     */
+    /** Initialize elements in media view holder */
     private fun initMediaViewHolderMocks() {
         whenever(seekBarViewModel.progress).thenReturn(seekBarData)
 
@@ -349,7 +370,8 @@
                         action1.id,
                         action2.id,
                         action3.id,
-                        action4.id)
+                        action4.id
+                    )
             }
 
         whenever(viewHolder.player).thenReturn(view)
@@ -394,9 +416,7 @@
         whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
     }
 
-    /**
-     * Initialize elements for the recommendation view holder
-     */
+    /** Initialize elements for the recommendation view holder */
     private fun initRecommendationViewHolderMocks() {
         recTitle1 = TextView(context)
         recTitle2 = TextView(context)
@@ -419,9 +439,8 @@
             .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
         whenever(recommendationViewHolder.mediaTitles)
             .thenReturn(listOf(recTitle1, recTitle2, recTitle3))
-        whenever(recommendationViewHolder.mediaSubtitles).thenReturn(
-            listOf(recSubtitle1, recSubtitle2, recSubtitle3)
-        )
+        whenever(recommendationViewHolder.mediaSubtitles)
+            .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
 
         whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
 
@@ -453,12 +472,13 @@
     fun bindSemanticActions() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", bg),
-            nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
-            custom0 = MediaAction(icon, null, "custom 0", bg),
-            custom1 = MediaAction(icon, null, "custom 1", bg)
-        )
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", bg),
+                nextOrCustom = MediaAction(icon, Runnable {}, "next", bg),
+                custom0 = MediaAction(icon, null, "custom 0", bg),
+                custom1 = MediaAction(icon, null, "custom 1", bg)
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
         player.bindPlayer(state, PACKAGE)
@@ -501,15 +521,16 @@
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
 
         // Setup button state: no prev or next button and their slots reserved
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", bg),
-            nextOrCustom = null,
-            prevOrCustom = null,
-            custom0 = MediaAction(icon, null, "custom 0", bg),
-            custom1 = MediaAction(icon, null, "custom 1", bg),
-            false,
-            true
-        )
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", bg),
+                nextOrCustom = null,
+                prevOrCustom = null,
+                custom0 = MediaAction(icon, null, "custom 0", bg),
+                custom1 = MediaAction(icon, null, "custom 1", bg),
+                false,
+                true
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -530,15 +551,16 @@
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
 
         // Setup button state: no prev or next button and their slots reserved
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", bg),
-            nextOrCustom = null,
-            prevOrCustom = null,
-            custom0 = MediaAction(icon, null, "custom 0", bg),
-            custom1 = MediaAction(icon, null, "custom 1", bg),
-            true,
-            false
-        )
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", bg),
+                nextOrCustom = null,
+                prevOrCustom = null,
+                custom0 = MediaAction(icon, null, "custom 0", bg),
+                custom1 = MediaAction(icon, null, "custom 1", bg),
+                true,
+                false
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -646,10 +668,11 @@
         useRealConstraintSets()
 
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(icon, Runnable {}, "play", null),
-            nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(
+                playOrPause = MediaAction(icon, Runnable {}, "play", null),
+                nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -719,9 +742,8 @@
         useRealConstraintSets()
 
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            nextOrCustom = MediaAction(icon, Runnable {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(nextOrCustom = MediaAction(icon, Runnable {}, "next", null))
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -736,10 +758,11 @@
     @Test
     fun bind_notScrubbing_scrubbingViewsGone() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = MediaAction(icon, {}, "prev", null),
+                nextOrCustom = MediaAction(icon, {}, "next", null)
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -770,10 +793,8 @@
     @Test
     fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = null,
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(prevOrCustom = null, nextOrCustom = MediaAction(icon, {}, "next", null))
         val state = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
         player.bindPlayer(state, PACKAGE)
@@ -790,10 +811,8 @@
     @Test
     fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = null
-        )
+        val semanticActions =
+            MediaButton(prevOrCustom = MediaAction(icon, {}, "prev", null), nextOrCustom = null)
         val state = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
         player.bindPlayer(state, PACKAGE)
@@ -810,10 +829,11 @@
     @Test
     fun setIsScrubbing_true_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = MediaAction(icon, {}, "prev", null),
+                nextOrCustom = MediaAction(icon, {}, "next", null)
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
         player.attachPlayer(viewHolder)
         player.bindPlayer(state, PACKAGE)
@@ -832,10 +852,11 @@
     @Test
     fun setIsScrubbing_trueThenFalse_scrubbingTimeGoneAtEnd() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(icon, {}, "prev", null),
-            nextOrCustom = MediaAction(icon, {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(
+                prevOrCustom = MediaAction(icon, {}, "prev", null),
+                nextOrCustom = MediaAction(icon, {}, "next", null)
+            )
         val state = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -859,18 +880,20 @@
     fun bindNotificationActions() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
         val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
-        val actions = listOf(
-            MediaAction(icon, Runnable {}, "previous", bg),
-            MediaAction(icon, Runnable {}, "play", bg),
-            MediaAction(icon, null, "next", bg),
-            MediaAction(icon, null, "custom 0", bg),
-            MediaAction(icon, Runnable {}, "custom 1", bg)
-        )
-        val state = mediaData.copy(
-            actions = actions,
-            actionsToShowInCompact = listOf(1, 2),
-            semanticActions = null
-        )
+        val actions =
+            listOf(
+                MediaAction(icon, Runnable {}, "previous", bg),
+                MediaAction(icon, Runnable {}, "play", bg),
+                MediaAction(icon, null, "next", bg),
+                MediaAction(icon, null, "custom 0", bg),
+                MediaAction(icon, Runnable {}, "custom 1", bg)
+            )
+        val state =
+            mediaData.copy(
+                actions = actions,
+                actionsToShowInCompact = listOf(1, 2),
+                semanticActions = null
+            )
 
         player.attachPlayer(viewHolder)
         player.bindPlayer(state, PACKAGE)
@@ -918,15 +941,12 @@
 
         val icon = context.getDrawable(R.drawable.ic_media_play)
         val bg = context.getDrawable(R.drawable.ic_media_play_container)
-        val semanticActions0 = MediaButton(
-            playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
-        )
-        val semanticActions1 = MediaButton(
-            playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null)
-        )
-        val semanticActions2 = MediaButton(
-            playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null)
-        )
+        val semanticActions0 =
+            MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
+        val semanticActions1 =
+            MediaButton(playOrPause = MediaAction(mockAvd1, Runnable {}, "pause", null))
+        val semanticActions2 =
+            MediaButton(playOrPause = MediaAction(mockAvd2, Runnable {}, "loading", null))
         val state0 = mediaData.copy(semanticActions = semanticActions0)
         val state1 = mediaData.copy(semanticActions = semanticActions1)
         val state2 = mediaData.copy(semanticActions = semanticActions2)
@@ -1089,11 +1109,10 @@
 
         val mockAvd0 = mock(AnimatedVectorDrawable::class.java)
         whenever(mockAvd0.mutate()).thenReturn(mockAvd0)
-        val semanticActions0 = MediaButton(
-                playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null)
-        )
-        val state = mediaData.copy(resumption = true, semanticActions = semanticActions0,
-                isPlaying = false)
+        val semanticActions0 =
+            MediaButton(playOrPause = MediaAction(mockAvd0, Runnable {}, "play", null))
+        val state =
+            mediaData.copy(resumption = true, semanticActions = semanticActions0, isPlaying = false)
         player.attachPlayer(viewHolder)
         player.bindPlayer(state, PACKAGE)
         assertThat(seamlessText.getText()).isEqualTo(APP_NAME)
@@ -1432,9 +1451,8 @@
 
     @Test
     fun actionPlayPauseClick_isLogged() {
-        val semanticActions = MediaButton(
-            playOrPause = MediaAction(null, Runnable {}, "play", null)
-        )
+        val semanticActions =
+            MediaButton(playOrPause = MediaAction(null, Runnable {}, "play", null))
         val data = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -1446,9 +1464,8 @@
 
     @Test
     fun actionPrevClick_isLogged() {
-        val semanticActions = MediaButton(
-            prevOrCustom = MediaAction(null, Runnable {}, "previous", null)
-        )
+        val semanticActions =
+            MediaButton(prevOrCustom = MediaAction(null, Runnable {}, "previous", null))
         val data = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -1460,9 +1477,8 @@
 
     @Test
     fun actionNextClick_isLogged() {
-        val semanticActions = MediaButton(
-            nextOrCustom = MediaAction(null, Runnable {}, "next", null)
-        )
+        val semanticActions =
+            MediaButton(nextOrCustom = MediaAction(null, Runnable {}, "next", null))
         val data = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -1474,9 +1490,8 @@
 
     @Test
     fun actionCustom0Click_isLogged() {
-        val semanticActions = MediaButton(
-            custom0 = MediaAction(null, Runnable {}, "custom 0", null)
-        )
+        val semanticActions =
+            MediaButton(custom0 = MediaAction(null, Runnable {}, "custom 0", null))
         val data = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -1488,9 +1503,8 @@
 
     @Test
     fun actionCustom1Click_isLogged() {
-        val semanticActions = MediaButton(
-            custom1 = MediaAction(null, Runnable {}, "custom 1", null)
-        )
+        val semanticActions =
+            MediaButton(custom1 = MediaAction(null, Runnable {}, "custom 1", null))
         val data = mediaData.copy(semanticActions = semanticActions)
 
         player.attachPlayer(viewHolder)
@@ -1502,13 +1516,14 @@
 
     @Test
     fun actionCustom2Click_isLogged() {
-        val actions = listOf(
-            MediaAction(null, Runnable {}, "action 0", null),
-            MediaAction(null, Runnable {}, "action 1", null),
-            MediaAction(null, Runnable {}, "action 2", null),
-            MediaAction(null, Runnable {}, "action 3", null),
-            MediaAction(null, Runnable {}, "action 4", null)
-        )
+        val actions =
+            listOf(
+                MediaAction(null, Runnable {}, "action 0", null),
+                MediaAction(null, Runnable {}, "action 1", null),
+                MediaAction(null, Runnable {}, "action 2", null),
+                MediaAction(null, Runnable {}, "action 3", null),
+                MediaAction(null, Runnable {}, "action 4", null)
+            )
         val data = mediaData.copy(actions = actions)
 
         player.attachPlayer(viewHolder)
@@ -1520,13 +1535,14 @@
 
     @Test
     fun actionCustom3Click_isLogged() {
-        val actions = listOf(
-            MediaAction(null, Runnable {}, "action 0", null),
-            MediaAction(null, Runnable {}, "action 1", null),
-            MediaAction(null, Runnable {}, "action 2", null),
-            MediaAction(null, Runnable {}, "action 3", null),
-            MediaAction(null, Runnable {}, "action 4", null)
-        )
+        val actions =
+            listOf(
+                MediaAction(null, Runnable {}, "action 0", null),
+                MediaAction(null, Runnable {}, "action 1", null),
+                MediaAction(null, Runnable {}, "action 2", null),
+                MediaAction(null, Runnable {}, "action 3", null),
+                MediaAction(null, Runnable {}, "action 4", null)
+            )
         val data = mediaData.copy(actions = actions)
 
         player.attachPlayer(viewHolder)
@@ -1538,13 +1554,14 @@
 
     @Test
     fun actionCustom4Click_isLogged() {
-        val actions = listOf(
-            MediaAction(null, Runnable {}, "action 0", null),
-            MediaAction(null, Runnable {}, "action 1", null),
-            MediaAction(null, Runnable {}, "action 2", null),
-            MediaAction(null, Runnable {}, "action 3", null),
-            MediaAction(null, Runnable {}, "action 4", null)
-        )
+        val actions =
+            listOf(
+                MediaAction(null, Runnable {}, "action 0", null),
+                MediaAction(null, Runnable {}, "action 1", null),
+                MediaAction(null, Runnable {}, "action 2", null),
+                MediaAction(null, Runnable {}, "action 3", null),
+                MediaAction(null, Runnable {}, "action 4", null)
+            )
         val data = mediaData.copy(actions = actions)
 
         player.attachPlayer(viewHolder)
@@ -1608,8 +1625,7 @@
 
         // THEN it shows without dismissing keyguard first
         captor.value.onClick(viewHolder.player)
-        verify(activityStarter).startActivity(eq(clickIntent), eq(true),
-                nullable(), eq(true))
+        verify(activityStarter).startActivity(eq(clickIntent), eq(true), nullable(), eq(true))
     }
 
     @Test
@@ -1697,20 +1713,22 @@
     fun bindRecommendation_listHasTooFewRecs_notDisplayed() {
         player.attachRecommendation(recommendationViewHolder)
         val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "title1")
-                    .setSubtitle("subtitle1")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("subtitle2")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("subtitle1")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("subtitle2")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                    )
             )
-        )
 
         player.bindRecommendation(data)
 
@@ -1722,30 +1740,32 @@
     fun bindRecommendation_listHasTooFewRecsWithIcons_notDisplayed() {
         player.attachRecommendation(recommendationViewHolder)
         val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "title1")
-                    .setSubtitle("subtitle1")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("subtitle2")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "empty icon 1")
-                    .setSubtitle("subtitle2")
-                    .setIcon(null)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "empty icon 2")
-                    .setSubtitle("subtitle2")
-                    .setIcon(null)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("subtitle1")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("subtitle2")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "empty icon 1")
+                            .setSubtitle("subtitle2")
+                            .setIcon(null)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "empty icon 2")
+                            .setSubtitle("subtitle2")
+                            .setIcon(null)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                    )
             )
-        )
 
         player.bindRecommendation(data)
 
@@ -1765,25 +1785,27 @@
         val subtitle3 = "Subtitle3"
         val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
 
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", title1)
-                    .setSubtitle(subtitle1)
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", title2)
-                    .setSubtitle(subtitle2)
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", title3)
-                    .setSubtitle(subtitle3)
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build()
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", title1)
+                            .setSubtitle(subtitle1)
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", title2)
+                            .setSubtitle(subtitle2)
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", title3)
+                            .setSubtitle(subtitle3)
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
             )
-        )
         player.bindRecommendation(data)
 
         assertThat(recTitle1.text).isEqualTo(title1)
@@ -1798,15 +1820,17 @@
     fun bindRecommendation_noTitle_subtitleNotShown() {
         player.attachRecommendation(recommendationViewHolder)
 
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build()
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
             )
-        )
         player.bindRecommendation(data)
 
         assertThat(recSubtitle1.text).isEqualTo("")
@@ -1818,25 +1842,27 @@
         player.attachRecommendation(recommendationViewHolder)
 
         val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "")
-                    .setSubtitle("fake subtitle")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build()
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "")
+                            .setSubtitle("fake subtitle")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
             )
-        )
         player.bindRecommendation(data)
 
         assertThat(expandedSet.getVisibility(recTitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
@@ -1850,25 +1876,27 @@
         player.attachRecommendation(recommendationViewHolder)
 
         val icon = Icon.createWithResource(context, R.drawable.ic_1x_mobiledata)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "title3")
-                    .setSubtitle("subtitle3")
-                    .setIcon(icon)
-                    .setExtras(Bundle.EMPTY)
-                    .build()
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "title3")
+                            .setSubtitle("subtitle3")
+                            .setIcon(icon)
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
             )
-        )
         player.bindRecommendation(data)
 
         assertThat(expandedSet.getVisibility(recSubtitle1.id)).isEqualTo(ConstraintSet.VISIBLE)
@@ -1880,25 +1908,27 @@
     fun bindRecommendation_noneHaveSubtitles_subtitleViewsGone() {
         useRealConstraintSets()
         player.attachRecommendation(recommendationViewHolder)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "title1")
-                    .setSubtitle("")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "title2")
-                    .setSubtitle("")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "title3")
-                    .setSubtitle("")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build()
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "title1")
+                            .setSubtitle("")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "title2")
+                            .setSubtitle("")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "title3")
+                            .setSubtitle("")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
             )
-        )
 
         player.bindRecommendation(data)
 
@@ -1911,25 +1941,27 @@
     fun bindRecommendation_noneHaveTitles_titleAndSubtitleViewsGone() {
         useRealConstraintSets()
         player.attachRecommendation(recommendationViewHolder)
-        val data = smartspaceData.copy(
-            recommendations = listOf(
-                SmartspaceAction.Builder("id1", "")
-                    .setSubtitle("subtitle1")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id2", "")
-                    .setSubtitle("subtitle2")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
-                    .setExtras(Bundle.EMPTY)
-                    .build(),
-                SmartspaceAction.Builder("id3", "")
-                    .setSubtitle("subtitle3")
-                    .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
-                    .setExtras(Bundle.EMPTY)
-                    .build()
+        val data =
+            smartspaceData.copy(
+                recommendations =
+                    listOf(
+                        SmartspaceAction.Builder("id1", "")
+                            .setSubtitle("subtitle1")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_1x_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id2", "")
+                            .setSubtitle("subtitle2")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_alarm))
+                            .setExtras(Bundle.EMPTY)
+                            .build(),
+                        SmartspaceAction.Builder("id3", "")
+                            .setSubtitle("subtitle3")
+                            .setIcon(Icon.createWithResource(context, R.drawable.ic_3g_mobiledata))
+                            .setExtras(Bundle.EMPTY)
+                            .build()
+                    )
             )
-        )
 
         player.bindRecommendation(data)
 
@@ -1942,20 +1974,23 @@
     }
 
     private fun getScrubbingChangeListener(): SeekBarViewModel.ScrubbingChangeListener =
-        withArgCaptor { verify(seekBarViewModel).setScrubbingChangeListener(capture()) }
+        withArgCaptor {
+            verify(seekBarViewModel).setScrubbingChangeListener(capture())
+        }
 
-    private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener =
-        withArgCaptor { verify(seekBarViewModel).setEnabledChangeListener(capture()) }
+    private fun getEnabledChangeListener(): SeekBarViewModel.EnabledChangeListener = withArgCaptor {
+        verify(seekBarViewModel).setEnabledChangeListener(capture())
+    }
 
     /**
-     *  Update our test to use real ConstraintSets instead of mocks.
+     * Update our test to use real ConstraintSets instead of mocks.
      *
-     *  Some item visibilities, such as the seekbar visibility, are dependent on other action's
-     *  visibilities. If we use mocks for the ConstraintSets, then action visibility changes are
-     *  just thrown away instead of being saved for reference later. This method sets us up to use
-     *  ConstraintSets so that we do save visibility changes.
+     * Some item visibilities, such as the seekbar visibility, are dependent on other action's
+     * visibilities. If we use mocks for the ConstraintSets, then action visibility changes are just
+     * thrown away instead of being saved for reference later. This method sets us up to use
+     * ConstraintSets so that we do save visibility changes.
      *
-     *  TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
+     * TODO(b/229740380): Can/should we use real expanded and collapsed sets for all tests?
      */
     private fun useRealConstraintSets() {
         expandedSet = ConstraintSet()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
similarity index 85%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
index 954b438..071604d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.graphics.Rect
 import android.provider.Settings
@@ -84,10 +84,8 @@
     private lateinit var statusBarCallback: ArgumentCaptor<(StatusBarStateController.StateListener)>
     @Captor
     private lateinit var dreamOverlayCallback:
-            ArgumentCaptor<(DreamOverlayStateController.Callback)>
-    @JvmField
-    @Rule
-    val mockito = MockitoJUnit.rule()
+        ArgumentCaptor<(DreamOverlayStateController.Callback)>
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
     private lateinit var mediaHierarchyManager: MediaHierarchyManager
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
@@ -98,13 +96,15 @@
 
     @Before
     fun setup() {
-        context.getOrCreateTestableResources().addOverride(
-                R.bool.config_use_split_notification_shade, false)
+        context
+            .getOrCreateTestableResources()
+            .addOverride(R.bool.config_use_split_notification_shade, false)
         mediaFrame = FrameLayout(context)
         testableLooper = TestableLooper.get(this)
         fakeHandler = FakeHandler(testableLooper.looper)
         whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
-        mediaHierarchyManager = MediaHierarchyManager(
+        mediaHierarchyManager =
+            MediaHierarchyManager(
                 context,
                 statusBarStateController,
                 keyguardStateController,
@@ -116,7 +116,8 @@
                 wakefulnessLifecycle,
                 notifPanelEvents,
                 settings,
-                fakeHandler,)
+                fakeHandler,
+            )
         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
         verify(statusBarStateController).addCallback(statusBarCallback.capture())
         verify(dreamOverlayStateController).addCallback(dreamOverlayCallback.capture())
@@ -125,7 +126,7 @@
         setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
         whenever(mediaCarouselController.mediaCarouselScrollHandler)
-                .thenReturn(mediaCarouselScrollHandler)
+            .thenReturn(mediaCarouselScrollHandler)
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         observer.onFinishedWakingUp()
@@ -151,30 +152,53 @@
     fun testBlockedWhenScreenTurningOff() {
         // Let's set it onto QS:
         mediaHierarchyManager.qsExpansion = 1.0f
-        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         observer.onStartedGoingToSleep()
         clearInvocations(mediaCarouselController)
         mediaHierarchyManager.qsExpansion = 0.0f
         verify(mediaCarouselController, times(0))
-                .onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
     }
 
     @Test
     fun testAllowedWhenNotTurningOff() {
         // Let's set it onto QS:
         mediaHierarchyManager.qsExpansion = 1.0f
-        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         clearInvocations(mediaCarouselController)
         mediaHierarchyManager.qsExpansion = 0.0f
-        verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                ArgumentMatchers.anyInt(),
+                any(MediaHostState::class.java),
+                anyBoolean(),
+                anyLong(),
+                anyLong()
+            )
     }
 
     @Test
@@ -183,22 +207,26 @@
 
         // Let's transition all the way to full shade
         mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
-        verify(mediaCarouselController).onDesiredLocationChanged(
-            eq(MediaHierarchyManager.LOCATION_QQS),
-            any(MediaHostState::class.java),
-            eq(false),
-            anyLong(),
-            anyLong())
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                eq(MediaHierarchyManager.LOCATION_QQS),
+                any(MediaHostState::class.java),
+                eq(false),
+                anyLong(),
+                anyLong()
+            )
         clearInvocations(mediaCarouselController)
 
         // Let's go back to the lock screen
         mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
-        verify(mediaCarouselController).onDesiredLocationChanged(
-            eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
-            any(MediaHostState::class.java),
-            eq(false),
-            anyLong(),
-            anyLong())
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
+                eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
+                any(MediaHostState::class.java),
+                eq(false),
+                anyLong(),
+                anyLong()
+            )
 
         // Let's make sure alpha is set
         mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
@@ -302,7 +330,7 @@
 
         val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
         assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
-                .isEqualTo(expectedTranslation)
+            .isEqualTo(expectedTranslation)
     }
 
     @Test
@@ -343,27 +371,31 @@
     fun testDream() {
         goToDream()
         setMediaDreamComplicationEnabled(true)
-        verify(mediaCarouselController).onDesiredLocationChanged(
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
                 eq(MediaHierarchyManager.LOCATION_DREAM_OVERLAY),
                 nullable(),
                 eq(false),
                 anyLong(),
-                anyLong())
+                anyLong()
+            )
         clearInvocations(mediaCarouselController)
 
         setMediaDreamComplicationEnabled(false)
-        verify(mediaCarouselController).onDesiredLocationChanged(
+        verify(mediaCarouselController)
+            .onDesiredLocationChanged(
                 eq(MediaHierarchyManager.LOCATION_QQS),
                 any(MediaHostState::class.java),
                 eq(false),
                 anyLong(),
-                anyLong())
+                anyLong()
+            )
     }
 
     private fun enableSplitShade() {
-        context.getOrCreateTestableResources().addOverride(
-            R.bool.config_use_split_notification_shade, true
-        )
+        context
+            .getOrCreateTestableResources()
+            .addOverride(R.bool.config_use_split_notification_shade, true)
         configurationController.notifyConfigurationChanged()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
new file mode 100644
index 0000000..32b822d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.controls.MediaTestUtils
+import com.android.systemui.media.controls.models.player.MediaData
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+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.mock
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+public class MediaPlayerDataTest : SysuiTestCase() {
+
+    @Mock private lateinit var playerIsPlaying: MediaControlPanel
+    private var systemClock: FakeSystemClock = FakeSystemClock()
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    companion object {
+        val LOCAL = MediaData.PLAYBACK_LOCAL
+        val REMOTE = MediaData.PLAYBACK_CAST_LOCAL
+        val RESUMPTION = true
+        val PLAYING = true
+        val UNDETERMINED = null
+    }
+
+    @Before
+    fun setup() {
+        MediaPlayerData.clear()
+    }
+
+    @Test
+    fun addPlayingThenRemote() {
+        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsRemote = mock(MediaControlPanel::class.java)
+        val dataIsRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsRemote,
+            playerIsRemote,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+
+        val players = MediaPlayerData.players()
+        assertThat(players).hasSize(2)
+        assertThat(players).containsExactly(playerIsPlaying, playerIsRemote).inOrder()
+    }
+
+    @Test
+    fun switchPlayersPlaying() {
+        val playerIsPlaying1 = mock(MediaControlPanel::class.java)
+        var dataIsPlaying1 = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsPlaying2 = mock(MediaControlPanel::class.java)
+        var dataIsPlaying2 = createMediaData("app2", !PLAYING, LOCAL, !RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying1,
+            playerIsPlaying1,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsPlaying2,
+            playerIsPlaying2,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        dataIsPlaying1 = createMediaData("app1", !PLAYING, LOCAL, !RESUMPTION)
+        dataIsPlaying2 = createMediaData("app2", PLAYING, LOCAL, !RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying1,
+            playerIsPlaying1,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsPlaying2,
+            playerIsPlaying2,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        val players = MediaPlayerData.players()
+        assertThat(players).hasSize(2)
+        assertThat(players).containsExactly(playerIsPlaying2, playerIsPlaying1).inOrder()
+    }
+
+    @Test
+    fun fullOrderTest() {
+        val dataIsPlaying = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsPlayingAndRemote = mock(MediaControlPanel::class.java)
+        val dataIsPlayingAndRemote = createMediaData("app2", PLAYING, REMOTE, !RESUMPTION)
+
+        val playerIsStoppedAndLocal = mock(MediaControlPanel::class.java)
+        val dataIsStoppedAndLocal = createMediaData("app3", !PLAYING, LOCAL, !RESUMPTION)
+
+        val playerIsStoppedAndRemote = mock(MediaControlPanel::class.java)
+        val dataIsStoppedAndRemote = createMediaData("app4", !PLAYING, REMOTE, !RESUMPTION)
+
+        val playerCanResume = mock(MediaControlPanel::class.java)
+        val dataCanResume = createMediaData("app5", !PLAYING, LOCAL, RESUMPTION)
+
+        val playerUndetermined = mock(MediaControlPanel::class.java)
+        val dataUndetermined = createMediaData("app6", UNDETERMINED, LOCAL, RESUMPTION)
+
+        MediaPlayerData.addMediaPlayer(
+            "3",
+            dataIsStoppedAndLocal,
+            playerIsStoppedAndLocal,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "5",
+            dataIsStoppedAndRemote,
+            playerIsStoppedAndRemote,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "4",
+            dataCanResume,
+            playerCanResume,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "1",
+            dataIsPlaying,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "2",
+            dataIsPlayingAndRemote,
+            playerIsPlayingAndRemote,
+            systemClock,
+            isSsReactivated = false
+        )
+        MediaPlayerData.addMediaPlayer(
+            "6",
+            dataUndetermined,
+            playerUndetermined,
+            systemClock,
+            isSsReactivated = false
+        )
+
+        val players = MediaPlayerData.players()
+        assertThat(players).hasSize(6)
+        assertThat(players)
+            .containsExactly(
+                playerIsPlaying,
+                playerIsPlayingAndRemote,
+                playerIsStoppedAndRemote,
+                playerIsStoppedAndLocal,
+                playerUndetermined,
+                playerCanResume
+            )
+            .inOrder()
+    }
+
+    @Test
+    fun testMoveMediaKeysAround() {
+        val keyA = "a"
+        val keyB = "b"
+
+        val data = createMediaData("app1", PLAYING, LOCAL, !RESUMPTION)
+
+        assertThat(MediaPlayerData.players()).hasSize(0)
+
+        MediaPlayerData.addMediaPlayer(
+            keyA,
+            data,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        assertThat(MediaPlayerData.players()).hasSize(1)
+        MediaPlayerData.addMediaPlayer(
+            keyB,
+            data,
+            playerIsPlaying,
+            systemClock,
+            isSsReactivated = false
+        )
+        systemClock.advanceTime(1)
+
+        assertThat(MediaPlayerData.players()).hasSize(2)
+
+        MediaPlayerData.moveIfExists(keyA, keyB)
+
+        assertThat(MediaPlayerData.players()).hasSize(1)
+
+        assertThat(MediaPlayerData.getMediaPlayer(keyA)).isNull()
+        assertThat(MediaPlayerData.getMediaPlayer(keyB)).isNotNull()
+    }
+
+    private fun createMediaData(
+        app: String,
+        isPlaying: Boolean?,
+        location: Int,
+        resumption: Boolean
+    ) =
+        MediaTestUtils.emptyMediaData.copy(
+            app = app,
+            packageName = "package: $app",
+            playbackLocation = location,
+            resumption = resumption,
+            notificationKey = "key: $app",
+            isPlaying = isPlaying
+        )
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
similarity index 91%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index 622a512..6b76155 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
@@ -22,13 +22,13 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.media.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.CONTROLS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DETAILS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.DURATION
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.MEDIATITLES_DELAY
-import com.android.systemui.media.MediaCarouselController.Companion.TRANSFORM_BEZIER
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.ANIMATION_BASE_DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.CONTROLS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DETAILS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.DURATION
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIACONTAINERS_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.MEDIATITLES_DELAY
+import com.android.systemui.media.controls.ui.MediaCarouselController.Companion.TRANSFORM_BEZIER
 import com.android.systemui.util.animation.MeasurementInput
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionViewState
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt
index 311aa96..323b781 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MetadataAnimationHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MetadataAnimationHandlerTest.kt
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.systemui.media
+package com.android.systemui.media.controls.ui
 
-import org.mockito.Mockito.`when` as whenever
 import android.animation.Animator
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
@@ -29,10 +28,11 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.times
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
@@ -55,8 +55,7 @@
         handler = MetadataAnimationHandler(exitAnimator, enterAnimator)
     }
 
-    @After
-    fun tearDown() {}
+    @After fun tearDown() {}
 
     @Test
     fun firstBind_startsAnimationSet() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt
similarity index 81%
rename from packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt
index d087b0f..d6cff81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/SquigglyProgressTest.kt
@@ -1,4 +1,20 @@
-package com.android.systemui.media
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.ui
 
 import android.graphics.Canvas
 import android.graphics.Color
@@ -107,7 +123,6 @@
         val (wavePaint, linePaint) = paintCaptor.getAllValues()
 
         assertThat(wavePaint.color).isEqualTo(tint)
-        assertThat(linePaint.color).isEqualTo(
-                ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA))
+        assertThat(linePaint.color).isEqualTo(ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA))
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
index 29188da..ce885c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
@@ -25,7 +25,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.util.animation.UniqueObjectHostView;
 
 import org.junit.Before;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index af53016..ed928a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -33,8 +33,8 @@
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.DreamMediaEntryComplication;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaData;
-import com.android.systemui.media.MediaDataManager;
+import com.android.systemui.media.controls.models.player.MediaData;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
index 7c83cb7..6a4c0f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt
@@ -22,6 +22,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -62,6 +65,34 @@
     }
 
     @Test
+    fun getIconFromPackageName_nullPackageName_returnsDefault() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, appPackageName = null, logger)
+
+        val expectedDesc =
+            ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
+                .loadContentDescription(context)
+        assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
+    }
+
+    @Test
+    fun getIconFromPackageName_invalidPackageName_returnsDefault() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, "fakePackageName", logger)
+
+        val expectedDesc =
+            ContentDescription.Resource(R.string.media_output_dialog_unknown_launch_app_name)
+                .loadContentDescription(context)
+        assertThat(icon.contentDescription.loadContentDescription(context)).isEqualTo(expectedDesc)
+    }
+
+    @Test
+    fun getIconFromPackageName_validPackageName_returnsAppInfo() {
+        val icon = MediaTttUtils.getIconFromPackageName(context, PACKAGE_NAME, logger)
+
+        assertThat(icon)
+            .isEqualTo(Icon.Loaded(appIconFromPackageName, ContentDescription.Loaded(APP_NAME)))
+    }
+
+    @Test
     fun getIconInfoFromPackageName_nullPackageName_returnsDefault() {
         val iconInfo =
             MediaTttUtils.getIconInfoFromPackageName(context, appPackageName = null, logger)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 110bbb8..fdeb3f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -17,14 +17,19 @@
 package com.android.systemui.media.taptotransfer.sender
 
 import android.app.StatusBarManager
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
 import android.media.MediaRoute2Info
 import android.os.PowerManager
+import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
@@ -32,16 +37,18 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.temporarydisplay.chipbar.ChipSenderInfo
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.google.common.truth.Truth.assertThat
@@ -60,20 +67,29 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 class MediaTttSenderCoordinatorTest : SysuiTestCase() {
+
+    // Note: This tests are a bit like integration tests because they use a real instance of
+    //   [ChipbarCoordinator] and verify that the coordinator displays the correct view, based on
+    //   the inputs from [MediaTttSenderCoordinator].
+
     private lateinit var underTest: MediaTttSenderCoordinator
 
     @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var applicationInfo: ApplicationInfo
     @Mock private lateinit var commandQueue: CommandQueue
     @Mock private lateinit var configurationController: ConfigurationController
     @Mock private lateinit var falsingManager: FalsingManager
     @Mock private lateinit var falsingCollector: FalsingCollector
     @Mock private lateinit var logger: MediaTttLogger
     @Mock private lateinit var mediaTttFlags: MediaTttFlags
+    @Mock private lateinit var packageManager: PackageManager
     @Mock private lateinit var powerManager: PowerManager
     @Mock private lateinit var viewUtil: ViewUtil
     @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var vibratorHelper: VibratorHelper
     private lateinit var chipbarCoordinator: ChipbarCoordinator
     private lateinit var commandQueueCallback: CommandQueue.Callbacks
+    private lateinit var fakeAppIconDrawable: Drawable
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
@@ -85,6 +101,18 @@
         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
         whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
 
+        fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
+        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
+        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
+        whenever(
+                packageManager.getApplicationInfo(
+                    eq(PACKAGE_NAME),
+                    any<PackageManager.ApplicationInfoFlags>()
+                )
+            )
+            .thenReturn(applicationInfo)
+        context.setMockPackageManager(packageManager)
+
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
@@ -100,10 +128,10 @@
                 accessibilityManager,
                 configurationController,
                 powerManager,
-                uiEventLogger,
                 falsingManager,
                 falsingCollector,
                 viewUtil,
+                vibratorHelper,
             )
         chipbarCoordinator.start()
 
@@ -149,10 +177,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME))
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -163,10 +198,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME))
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -177,12 +219,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -193,12 +240,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -209,12 +261,66 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id)
+        verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_nullUndoCallback_noUndo() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ null
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_withUndoRunnable_undoVisible() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
+        var undoCallbackCalled = false
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {
+                    undoCallbackCalled = true
+                }
+            },
+        )
+
+        getChipbarView().getUndoButton().performClick()
+
+        // Event index 1 since initially displaying the succeeded chip would also log an event
+        assertThat(uiEventLoggerFake.eventId(1))
+            .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id)
+        assertThat(undoCallbackCalled).isTrue()
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
     }
 
     @Test
@@ -225,12 +331,68 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id)
+        verify(vibratorHelper, never()).vibrate(any<VibrationEffect>())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_nullUndoCallback_noUndo() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ null
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_withUndoRunnable_undoVisible() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getUndoButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
+        var undoCallbackCalled = false
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            /* undoCallback= */ object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {
+                    undoCallbackCalled = true
+                }
+            },
+        )
+
+        getChipbarView().getUndoButton().performClick()
+
+        // Event index 1 since initially displaying the succeeded chip would also log an event
+        assertThat(uiEventLoggerFake.eventId(1))
+            .isEqualTo(
+                MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
+            )
+        assertThat(undoCallbackCalled).isTrue()
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
     }
 
     @Test
@@ -241,12 +403,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -257,12 +424,17 @@
             null
         )
 
-        assertThat(getChipView().getChipText())
-            .isEqualTo(
-                transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME)
-            )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
+        assertThat(chipbarView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.getExpectedStateText())
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getUndoButton().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
         assertThat(uiEventLoggerFake.eventId(0))
             .isEqualTo(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id)
+        verify(vibratorHelper).vibrate(any<VibrationEffect>())
     }
 
     @Test
@@ -407,53 +579,113 @@
         verify(windowManager).removeView(any())
     }
 
-    private fun getChipView(): ViewGroup {
+    @Test
+    fun transferToReceiverSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED,
+            routeInfo,
+            object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.getExpectedStateText())
+
+        // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should
+        // verify that the new state it triggers operates just like any other state.
+        getChipbarView().getUndoButton().performClick()
+        fakeExecutor.runAllReady()
+
+        // Verify that the click updated us to the triggered state
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.getExpectedStateText())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        // Verify that we didn't remove the chipbar because it's in the triggered state
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        // Verify we eventually remove the chipbar
+        verify(windowManager).removeView(any())
+    }
+
+    @Test
+    fun transferToThisDeviceSucceeded_thenUndo_thenFar_viewStillDisplayedButDoesTimeOut() {
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED,
+            routeInfo,
+            object : IUndoMediaTransferCallback.Stub() {
+                override fun onUndoTriggered() {}
+            },
+        )
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.getExpectedStateText())
+
+        // Because [MediaTttSenderCoordinator] internally creates the undo callback, we should
+        // verify that the new state it triggers operates just like any other state.
+        getChipbarView().getUndoButton().performClick()
+        fakeExecutor.runAllReady()
+
+        // Verify that the click updated us to the triggered state
+        assertThat(chipbarView.getChipText())
+            .isEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText())
+
+        commandQueueCallback.updateMediaTapToTransferSenderDisplay(
+            StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER,
+            routeInfo,
+            null
+        )
+        fakeExecutor.runAllReady()
+
+        // Verify that we didn't remove the chipbar because it's in the triggered state
+        verify(windowManager, never()).removeView(any())
+        verify(logger).logRemovalBypass(any(), any())
+
+        fakeClock.advanceTime(TIMEOUT + 1L)
+
+        // Verify we eventually remove the chipbar
+        verify(windowManager).removeView(any())
+    }
+
+    private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
 
+    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.start_icon)
+
     private fun ViewGroup.getChipText(): String =
         (this.requireViewById<TextView>(R.id.text)).text as String
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToStartCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo)
+    private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToEndCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
+    private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
+    private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.end_button)
 
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
+    private fun ChipStateSender.getExpectedStateText(): String? {
+        return this.getChipTextString(context, OTHER_DEVICE_NAME).loadText(context)
+    }
 }
 
+private const val APP_NAME = "Fake app name"
 private const val OTHER_DEVICE_NAME = "My Tablet"
+private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
 
 private val routeInfo =
     MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
         .addFeature("feature")
-        .setClientPackageName("com.android.systemui")
+        .setClientPackageName(PACKAGE_NAME)
         .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
index 0badd861..1bc4719 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -147,6 +147,18 @@
     }
 
     @Test
+    public void testMonochromatic() {
+        int colorInt = 0xffB3588A; // H350 C50 T50
+        ColorScheme colorScheme = new ColorScheme(colorInt, false /* darkTheme */,
+                Style.MONOCHROMATIC /* style */);
+        int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
+        Assert.assertTrue(
+                Color.red(neutralMid) == Color.green(neutralMid)
+                && Color.green(neutralMid) == Color.blue(neutralMid)
+        );
+    }
+
+    @Test
     @SuppressWarnings("ResultOfMethodCallIgnored")
     public void testToString() {
         new ColorScheme(Color.TRANSPARENT, false /* darkTheme */).toString();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index d2c2d58..cd7a949 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -50,7 +50,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index b847ad0..caf8321 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -44,7 +44,7 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.media.MediaHost;
+import com.android.systemui.media.controls.ui.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTileView;
 import com.android.systemui.qs.customize.QSCustomizerController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index e539705..3c867ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -7,8 +7,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.media.MediaHost
-import com.android.systemui.media.MediaHostState
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.customize.QSCustomizerController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
index 1c686c6..5e9c1aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSecurityFooterTest.java
@@ -22,7 +22,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -52,6 +51,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
 import android.widget.TextView;
 
 import com.android.systemui.R;
@@ -97,6 +97,7 @@
     private static final int DEFAULT_ICON_ID = R.drawable.ic_info_outline;
 
     private ViewGroup mRootView;
+    private ViewGroup mSecurityFooterView;
     private TextView mFooterText;
     private TestableImageView mPrimaryFooterIcon;
     private QSSecurityFooter mFooter;
@@ -121,21 +122,26 @@
         Looper looper = mTestableLooper.getLooper();
         Handler mainHandler = new Handler(looper);
         when(mUserTracker.getUserInfo()).thenReturn(mock(UserInfo.class));
-        mRootView = (ViewGroup) new LayoutInflaterBuilder(mContext)
+        mSecurityFooterView = (ViewGroup) new LayoutInflaterBuilder(mContext)
                 .replace("ImageView", TestableImageView.class)
                 .build().inflate(R.layout.quick_settings_security_footer, null, false);
         mFooterUtils = new QSSecurityFooterUtils(getContext(),
                 getContext().getSystemService(DevicePolicyManager.class), mUserTracker,
                 mainHandler, mActivityStarter, mSecurityController, looper, mDialogLaunchAnimator);
-        mFooter = new QSSecurityFooter(mRootView, mainHandler, mSecurityController, looper,
-                mBroadcastDispatcher, mFooterUtils);
-        mFooterText = mRootView.findViewById(R.id.footer_text);
-        mPrimaryFooterIcon = mRootView.findViewById(R.id.primary_footer_icon);
+        mFooter = new QSSecurityFooter(mSecurityFooterView, mainHandler, mSecurityController,
+                looper, mBroadcastDispatcher, mFooterUtils);
+        mFooterText = mSecurityFooterView.findViewById(R.id.footer_text);
+        mPrimaryFooterIcon = mSecurityFooterView.findViewById(R.id.primary_footer_icon);
 
         when(mSecurityController.getDeviceOwnerComponentOnAnyUser())
                 .thenReturn(DEVICE_OWNER_COMPONENT);
         when(mSecurityController.getDeviceOwnerType(DEVICE_OWNER_COMPONENT))
                 .thenReturn(DEVICE_OWNER_TYPE_DEFAULT);
+
+        // mSecurityFooterView must have a ViewGroup parent so that
+        // DialogLaunchAnimator.Controller.fromView() does not return null.
+        mRootView = new FrameLayout(mContext);
+        mRootView.addView(mSecurityFooterView);
         ViewUtils.attachView(mRootView);
 
         mFooter.init();
@@ -153,7 +159,7 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertEquals(View.GONE, mRootView.getVisibility());
+        assertEquals(View.GONE, mSecurityFooterView.getVisibility());
     }
 
     @Test
@@ -165,7 +171,7 @@
         TestableLooper.get(this).processAllMessages();
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_management),
                      mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -181,7 +187,7 @@
         assertEquals(mContext.getString(R.string.quick_settings_disclosure_named_management,
                                         MANAGING_ORGANIZATION),
                 mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -200,7 +206,7 @@
         assertEquals(mContext.getString(
                 R.string.quick_settings_financed_disclosure_named_management,
                 MANAGING_ORGANIZATION), mFooterText.getText());
-        assertEquals(View.VISIBLE, mRootView.getVisibility());
+        assertEquals(View.VISIBLE, mSecurityFooterView.getVisibility());
         assertEquals(View.VISIBLE, mPrimaryFooterIcon.getVisibility());
         assertEquals(DEFAULT_ICON_ID, mPrimaryFooterIcon.getLastImageResource());
     }
@@ -217,7 +223,7 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertEquals(View.GONE, mRootView.getVisibility());
+        assertEquals(View.GONE, mSecurityFooterView.getVisibility());
     }
 
     @Test
@@ -227,8 +233,8 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertFalse(mRootView.isClickable());
-        assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertFalse(mSecurityFooterView.isClickable());
+        assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -241,8 +247,9 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertTrue(mRootView.isClickable());
-        assertEquals(View.VISIBLE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertTrue(mSecurityFooterView.isClickable());
+        assertEquals(View.VISIBLE,
+                mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -254,8 +261,8 @@
         mFooter.refreshState();
 
         TestableLooper.get(this).processAllMessages();
-        assertFalse(mRootView.isClickable());
-        assertEquals(View.GONE, mRootView.findViewById(R.id.footer_icon).getVisibility());
+        assertFalse(mSecurityFooterView.isClickable());
+        assertEquals(View.GONE, mSecurityFooterView.findViewById(R.id.footer_icon).getVisibility());
     }
 
     @Test
@@ -734,11 +741,11 @@
     @Test
     public void testDialogUsesDialogLauncher() {
         when(mSecurityController.isDeviceManaged()).thenReturn(true);
-        mFooter.onClick(mRootView);
+        mFooter.onClick(mSecurityFooterView);
 
         mTestableLooper.processAllMessages();
 
-        verify(mDialogLaunchAnimator).showFromView(any(), eq(mRootView), any());
+        verify(mDialogLaunchAnimator).show(any(), any());
     }
 
     @Test
@@ -775,7 +782,7 @@
         ArgumentCaptor<AlertDialog> dialogCaptor = ArgumentCaptor.forClass(AlertDialog.class);
 
         mTestableLooper.processAllMessages();
-        verify(mDialogLaunchAnimator).showFromView(dialogCaptor.capture(), any(), any());
+        verify(mDialogLaunchAnimator).show(dialogCaptor.capture(), any());
 
         AlertDialog dialog = dialogCaptor.getValue();
         dialog.create();
@@ -817,8 +824,8 @@
         verify(mBroadcastDispatcher).registerReceiverWithHandler(captor.capture(), any(), any(),
                 any());
 
-        // Pretend view is not visible temporarily
-        mRootView.onVisibilityAggregated(false);
+        // Pretend view is not attached anymore.
+        mRootView.removeView(mSecurityFooterView);
         captor.getValue().onReceive(mContext,
                 new Intent(DevicePolicyManager.ACTION_SHOW_DEVICE_MONITORING_DIALOG));
         mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 3c58b6fc..c452872 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -52,6 +52,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.dump.nano.SystemUIProtoDump;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.qs.QSTile;
@@ -114,8 +115,6 @@
     @Mock
     private DumpManager mDumpManager;
     @Mock
-    private QSTile.State mMockState;
-    @Mock
     private CentralSurfaces mCentralSurfaces;
     @Mock
     private QSLogger mQSLogger;
@@ -195,7 +194,6 @@
     }
 
     private void setUpTileFactory() {
-        when(mMockState.toString()).thenReturn(MOCK_STATE_STRING);
         // Only create this kind of tiles
         when(mDefaultFactory.createTile(anyString())).thenAnswer(
                 invocation -> {
@@ -209,7 +207,11 @@
                     } else if ("na".equals(spec)) {
                         return new NotAvailableTile(mQSTileHost);
                     } else if (CUSTOM_TILE_SPEC.equals(spec)) {
-                        return mCustomTile;
+                        QSTile tile = mCustomTile;
+                        QSTile.State s = mock(QSTile.State.class);
+                        s.spec = spec;
+                        when(mCustomTile.getState()).thenReturn(s);
+                        return tile;
                     } else if ("internet".equals(spec)
                             || "wifi".equals(spec)
                             || "cell".equals(spec)) {
@@ -647,7 +649,7 @@
     @Test
     public void testSetTileRemoved_removedBySystem() {
         int user = mUserTracker.getUserId();
-        saveSetting("spec1" + CUSTOM_TILE_SPEC);
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
 
         // This will be done by TileServiceManager
         mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
@@ -658,6 +660,27 @@
                 .getBoolean(CUSTOM_TILE.flattenToString(), false));
     }
 
+    @Test
+    public void testProtoDump_noTiles() {
+        SystemUIProtoDump proto = new SystemUIProtoDump();
+        mQSTileHost.dumpProto(proto, new String[0]);
+
+        assertEquals(0, proto.tiles.length);
+    }
+
+    @Test
+    public void testTilesInOrder() {
+        saveSetting("spec1," + CUSTOM_TILE_SPEC);
+
+        SystemUIProtoDump proto = new SystemUIProtoDump();
+        mQSTileHost.dumpProto(proto, new String[0]);
+
+        assertEquals(2, proto.tiles.length);
+        assertEquals("spec1", proto.tiles[0].getSpec());
+        assertEquals(CUSTOM_TILE.getPackageName(), proto.tiles[1].getComponentName().packageName);
+        assertEquals(CUSTOM_TILE.getClassName(), proto.tiles[1].getComponentName().className);
+    }
+
     private SharedPreferences getSharedPreferenecesForUser(int user) {
         return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user);
     }
@@ -707,12 +730,9 @@
 
         @Override
         public State newTileState() {
-            return mMockState;
-        }
-
-        @Override
-        public State getState() {
-            return mMockState;
+            State s = mock(QSTile.State.class);
+            when(s.toString()).thenReturn(MOCK_STATE_STRING);
+            return s;
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 6af8e49..f53e997 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -23,8 +23,8 @@
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.media.MediaHost
-import com.android.systemui.media.MediaHostState
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.controls.ui.MediaHostState
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.plugins.qs.QSTileView
 import com.android.systemui.qs.customize.QSCustomizerController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
new file mode 100644
index 0000000..629c663
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileStateToProtoTest.kt
@@ -0,0 +1,104 @@
+package com.android.systemui.qs
+
+import android.content.ComponentName
+import android.service.quicksettings.Tile
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.external.CustomTile
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TileStateToProtoTest : SysuiTestCase() {
+
+    companion object {
+        private const val TEST_LABEL = "label"
+        private const val TEST_SUBTITLE = "subtitle"
+        private const val TEST_SPEC = "spec"
+        private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
+    }
+
+    @Test
+    fun platformTile_INACTIVE() {
+        val state =
+            QSTile.State().apply {
+                spec = TEST_SPEC
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_INACTIVE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isTrue()
+        assertThat(proto?.spec).isEqualTo(TEST_SPEC)
+        assertThat(proto?.hasComponentName()).isFalse()
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_INACTIVE)
+        assertThat(proto?.hasBooleanState()).isFalse()
+    }
+
+    @Test
+    fun componentTile_UNAVAILABLE() {
+        val state =
+            QSTile.State().apply {
+                spec = CustomTile.toSpec(TEST_COMPONENT)
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_UNAVAILABLE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isFalse()
+        assertThat(proto?.hasComponentName()).isTrue()
+        val componentName = proto?.componentName
+        assertThat(componentName?.packageName).isEqualTo(TEST_COMPONENT.packageName)
+        assertThat(componentName?.className).isEqualTo(TEST_COMPONENT.className)
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_UNAVAILABLE)
+        assertThat(proto?.hasBooleanState()).isFalse()
+    }
+
+    @Test
+    fun booleanState_ACTIVE() {
+        val state =
+            QSTile.BooleanState().apply {
+                spec = TEST_SPEC
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_ACTIVE
+                value = true
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNotNull()
+        assertThat(proto?.hasSpec()).isTrue()
+        assertThat(proto?.spec).isEqualTo(TEST_SPEC)
+        assertThat(proto?.hasComponentName()).isFalse()
+        assertThat(proto?.label).isEqualTo(TEST_LABEL)
+        assertThat(proto?.secondaryLabel).isEqualTo(TEST_SUBTITLE)
+        assertThat(proto?.state).isEqualTo(Tile.STATE_ACTIVE)
+        assertThat(proto?.hasBooleanState()).isTrue()
+        assertThat(proto?.booleanState).isTrue()
+    }
+
+    @Test
+    fun noSpec_returnsNull() {
+        val state =
+            QSTile.State().apply {
+                label = TEST_LABEL
+                secondaryLabel = TEST_SUBTITLE
+                state = Tile.STATE_ACTIVE
+            }
+        val proto = state.toProto()
+
+        assertThat(proto).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
index 3c25807..2c2ddbb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/domain/interactor/FooterActionsInteractorTest.kt
@@ -23,13 +23,13 @@
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.nano.MetricsProto
 import com.android.internal.logging.testing.FakeMetricsLogger
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.globalactions.GlobalActionsDialogLite
@@ -70,13 +70,13 @@
         val underTest = utils.footerActionsInteractor(qsSecurityFooterUtils = qsSecurityFooterUtils)
 
         val quickSettingsContext = mock<Context>()
-        underTest.showDeviceMonitoringDialog(quickSettingsContext)
+
+        underTest.showDeviceMonitoringDialog(quickSettingsContext, null)
         verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
 
-        val view = mock<View>()
-        whenever(view.context).thenReturn(quickSettingsContext)
-        underTest.showDeviceMonitoringDialog(view)
-        verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, null)
+        val expandable = mock<Expandable>()
+        underTest.showDeviceMonitoringDialog(quickSettingsContext, expandable)
+        verify(qsSecurityFooterUtils).showDeviceMonitoringDialog(quickSettingsContext, expandable)
     }
 
     @Test
@@ -85,8 +85,8 @@
         val underTest = utils.footerActionsInteractor(uiEventLogger = uiEventLogger)
 
         val globalActionsDialogLite = mock<GlobalActionsDialogLite>()
-        val view = mock<View>()
-        underTest.showPowerMenuDialog(globalActionsDialogLite, view)
+        val expandable = mock<Expandable>()
+        underTest.showPowerMenuDialog(globalActionsDialogLite, expandable)
 
         // Event is logged.
         val logs = uiEventLogger.logs
@@ -99,7 +99,7 @@
             .showOrHideDialog(
                 /* keyguardShowing= */ false,
                 /* isDeviceProvisioned= */ true,
-                view,
+                expandable,
             )
     }
 
@@ -167,11 +167,11 @@
                 userSwitchDialogController = userSwitchDialogController,
             )
 
-        val view = mock<View>()
-        underTest.showUserSwitcher(view)
+        val expandable = mock<Expandable>()
+        underTest.showUserSwitcher(context, expandable)
 
         // Dialog is shown.
-        verify(userSwitchDialogController).showDialog(view)
+        verify(userSwitchDialogController).showDialog(context, expandable)
     }
 
     @Test
@@ -184,12 +184,9 @@
                 activityStarter = activityStarter,
             )
 
-        // The clicked view. The context is necessary because it's used to build the intent, that
-        // we check below.
-        val view = mock<View>()
-        whenever(view.context).thenReturn(context)
-
-        underTest.showUserSwitcher(view)
+        // The clicked expandable.
+        val expandable = mock<Expandable>()
+        underTest.showUserSwitcher(context, expandable)
 
         // Dialog is shown.
         val intentCaptor = argumentCaptor<Intent>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index bc27bbc..3131f60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
@@ -41,20 +42,27 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class UserDetailViewAdapterTest : SysuiTestCase() {
 
-    @Mock private lateinit var mUserSwitcherController: UserSwitcherController
-    @Mock private lateinit var mParent: ViewGroup
-    @Mock private lateinit var mUserDetailItemView: UserDetailItemView
-    @Mock private lateinit var mOtherView: View
-    @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
-    @Mock private lateinit var mLayoutInflater: LayoutInflater
+    @Mock
+    private lateinit var mUserSwitcherController: UserSwitcherController
+    @Mock
+    private lateinit var mParent: ViewGroup
+    @Mock
+    private lateinit var mUserDetailItemView: UserDetailItemView
+    @Mock
+    private lateinit var mOtherView: View
+    @Mock
+    private lateinit var mInflatedUserDetailItemView: UserDetailItemView
+    @Mock
+    private lateinit var mLayoutInflater: LayoutInflater
     private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
     private lateinit var adapter: UserDetailView.Adapter
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -67,10 +75,12 @@
 
         mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater)
         `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean()))
-                .thenReturn(mInflatedUserDetailItemView)
+            .thenReturn(mInflatedUserDetailItemView)
         `when`(mParent.context).thenReturn(mContext)
-        adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger,
-                falsingManagerFake)
+        adapter = UserDetailView.Adapter(
+            mContext, mUserSwitcherController, uiEventLogger,
+            falsingManagerFake
+        )
         mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user))
     }
 
@@ -145,6 +155,15 @@
         assertNull(adapter.users.find { it.isManageUsers })
     }
 
+    @Test
+    fun clickDismissDialog() {
+        val shower: UserSwitchDialogController.DialogShower =
+            mock(UserSwitchDialogController.DialogShower::class.java)
+        adapter.injectDialogShower(shower)
+        adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
+        verify(shower).dismiss()
+    }
+
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index 9d908fd..0a34810 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -20,12 +20,12 @@
 import android.content.Intent
 import android.provider.Settings
 import android.testing.AndroidTestingRunner
-import android.view.View
 import android.widget.Button
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.animation.Expandable
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PseudoGridView
@@ -35,6 +35,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -63,7 +64,7 @@
     @Mock
     private lateinit var userDetailViewAdapter: UserDetailView.Adapter
     @Mock
-    private lateinit var launchView: View
+    private lateinit var launchExpandable: Expandable
     @Mock
     private lateinit var neutralButton: Button
     @Mock
@@ -79,7 +80,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(launchView.context).thenReturn(mContext)
         `when`(dialog.context).thenReturn(mContext)
 
         controller = UserSwitchDialogController(
@@ -94,32 +94,34 @@
 
     @Test
     fun showDialog_callsDialogShow() {
-        controller.showDialog(launchView)
-        verify(dialogLaunchAnimator).showFromView(eq(dialog), eq(launchView), any(), anyBoolean())
+        val launchController = mock<DialogLaunchAnimator.Controller>()
+        `when`(launchExpandable.dialogLaunchController(any())).thenReturn(launchController)
+        controller.showDialog(context, launchExpandable)
+        verify(dialogLaunchAnimator).show(eq(dialog), eq(launchController), anyBoolean())
         verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN)
     }
 
     @Test
     fun dialog_showForAllUsers() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(dialog).setShowForAllUsers(true)
     }
 
     @Test
     fun dialog_cancelOnTouchOutside() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(dialog).setCanceledOnTouchOutside(true)
     }
 
     @Test
     fun adapterAndGridLinked() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
         verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>())
     }
 
     @Test
     fun doneButtonLogsCorrectly() {
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor))
 
@@ -132,7 +134,7 @@
     fun clickSettingsButton_noFalsing_opensSettings() {
         `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false)
 
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog)
             .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
@@ -153,7 +155,7 @@
     fun clickSettingsButton_Falsing_notOpensSettings() {
         `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true)
 
-        controller.showDialog(launchView)
+        controller.showDialog(context, launchExpandable)
 
         verify(dialog)
             .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
new file mode 100644
index 0000000..1130bda
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings.brightness
+
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+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.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class BrightnessDialogTest : SysuiTestCase() {
+
+    @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory
+    @Mock private lateinit var backgroundHandler: Handler
+    @Mock private lateinit var brightnessSliderController: BrightnessSliderController
+
+    @Rule
+    @JvmField
+    var activityRule =
+        ActivityTestRule(
+            object : SingleActivityFactory<TestDialog>(TestDialog::class.java) {
+                override fun create(intent: Intent?): TestDialog {
+                    return TestDialog(
+                        fakeBroadcastDispatcher,
+                        brightnessSliderControllerFactory,
+                        backgroundHandler
+                    )
+                }
+            },
+            false,
+            false
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        `when`(brightnessSliderControllerFactory.create(any(), any()))
+            .thenReturn(brightnessSliderController)
+        `when`(brightnessSliderController.rootView).thenReturn(View(context))
+
+        activityRule.launchActivity(null)
+    }
+
+    @After
+    fun tearDown() {
+        activityRule.finishActivity()
+    }
+
+    @Test
+    fun testGestureExclusion() {
+        val frame = activityRule.activity.requireViewById<View>(R.id.brightness_mirror_container)
+
+        val lp = frame.layoutParams as ViewGroup.MarginLayoutParams
+        val horizontalMargin =
+            activityRule.activity.resources.getDimensionPixelSize(
+                R.dimen.notification_side_paddings
+            )
+        assertThat(lp.leftMargin).isEqualTo(horizontalMargin)
+        assertThat(lp.rightMargin).isEqualTo(horizontalMargin)
+
+        assertThat(frame.systemGestureExclusionRects.size).isEqualTo(1)
+        val exclusion = frame.systemGestureExclusionRects[0]
+        assertThat(exclusion)
+            .isEqualTo(Rect(-horizontalMargin, 0, frame.width + horizontalMargin, frame.height))
+    }
+
+    class TestDialog(
+        broadcastDispatcher: BroadcastDispatcher,
+        brightnessSliderControllerFactory: BrightnessSliderController.Factory,
+        backgroundHandler: Handler
+    ) : BrightnessDialog(broadcastDispatcher, brightnessSliderControllerFactory, backgroundHandler)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index ac02af87..02f28a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -18,6 +18,7 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
+import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -33,6 +34,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
@@ -77,6 +79,7 @@
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.keyguard.KeyguardClockSwitch;
 import com.android.keyguard.KeyguardClockSwitchController;
 import com.android.keyguard.KeyguardStatusView;
@@ -94,7 +97,6 @@
 import com.android.systemui.camera.CameraGestureHelper;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
@@ -103,14 +105,15 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
-import com.android.systemui.media.KeyguardMediaController;
-import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaHierarchyManager;
+import com.android.systemui.media.controls.pipeline.MediaDataManager;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.MediaHierarchyManager;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
-import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -166,7 +169,6 @@
 import com.android.systemui.unfold.SysUIUnfoldComponent;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
-import com.android.systemui.wallet.controller.QuickAccessWalletController;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
 import org.junit.After;
@@ -174,6 +176,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -253,17 +256,15 @@
     @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private PrivacyDotViewController mPrivacyDotViewController;
     @Mock private NavigationModeController mNavigationModeController;
+    @Mock private NavigationBarController mNavigationBarController;
     @Mock private LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
     @Mock private ContentResolver mContentResolver;
     @Mock private TapAgainViewController mTapAgainViewController;
     @Mock private KeyguardIndicationController mKeyguardIndicationController;
     @Mock private FragmentService mFragmentService;
     @Mock private FragmentHostManager mFragmentHostManager;
-    @Mock private QuickAccessWalletController mQuickAccessWalletController;
-    @Mock private QRCodeScannerController mQrCodeScannerController;
     @Mock private NotificationRemoteInputManager mNotificationRemoteInputManager;
     @Mock private RecordingController mRecordingController;
-    @Mock private ControlsComponent mControlsComponent;
     @Mock private LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock private DumpManager mDumpManager;
     @Mock private InteractionJankMonitor mInteractionJankMonitor;
@@ -284,6 +285,10 @@
     @Mock private ViewTreeObserver mViewTreeObserver;
     @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+    @Mock private MotionEvent mDownMotionEvent;
+    @Captor
+    private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
+            mEmptySpaceClickListenerCaptor;
 
     private NotificationPanelViewController.TouchHandler mTouchHandler;
     private ConfigurationController mConfigurationController;
@@ -375,6 +380,7 @@
 
         NotificationWakeUpCoordinator coordinator =
                 new NotificationWakeUpCoordinator(
+                        mDumpManager,
                         mock(HeadsUpManagerPhone.class),
                         new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager,
                                 mInteractionJankMonitor),
@@ -390,6 +396,7 @@
                 mConfigurationController,
                 mStatusBarStateController,
                 mFalsingManager,
+                mShadeExpansionStateManager,
                 mLockscreenShadeTransitionController,
                 new FalsingCollectorFake(),
                 mDumpManager);
@@ -427,6 +434,8 @@
         when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
         when(mView.getParent()).thenReturn(mViewParent);
         when(mQs.getHeader()).thenReturn(mQsHeader);
+        when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN);
+        when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
 
         mMainHandler = new Handler(Looper.getMainLooper());
         NotificationPanelViewController.PanelEventsEmitter panelEventsEmitter =
@@ -470,6 +479,7 @@
                 mPrivacyDotViewController,
                 mTapAgainViewController,
                 mNavigationModeController,
+                mNavigationBarController,
                 mFragmentService,
                 mContentResolver,
                 mRecordingController,
@@ -514,6 +524,8 @@
                 .addCallback(mNotificationPanelViewController.mStatusBarStateListener);
         mNotificationPanelViewController
                 .setHeadsUpAppearanceController(mock(HeadsUpAppearanceController.class));
+        verify(mNotificationStackScrollLayoutController)
+                .setOnEmptySpaceClickListener(mEmptySpaceClickListenerCaptor.capture());
     }
 
     @After
@@ -752,6 +764,38 @@
     }
 
     @Test
+    public void testOnTouchEvent_expansionResumesAfterBriefTouch() {
+        // Start shade collapse with swipe up
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
+                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+        onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        assertThat(mNotificationPanelViewController.getClosing()).isTrue();
+        assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue();
+
+        // simulate touch that does not exceed touch slop
+        onTouchEvent(MotionEvent.obtain(2L /* downTime */,
+                2L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        mNotificationPanelViewController.setTouchSlopExceeded(false);
+
+        onTouchEvent(MotionEvent.obtain(2L /* downTime */,
+                2L /* eventTime */, MotionEvent.ACTION_UP, 0f /* x */, 300f /* y */,
+                0 /* metaState */));
+
+        // fling should still be called after a touch that does not exceed touch slop
+        assertThat(mNotificationPanelViewController.getClosing()).isTrue();
+        assertThat(mNotificationPanelViewController.getIsFlinging()).isTrue();
+    }
+
+    @Test
     public void handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
         when(mCommandQueue.panelsEnabled()).thenReturn(false);
 
@@ -1542,6 +1586,76 @@
         );
     }
 
+    @Test
+    public void onEmptySpaceClicked_notDozingAndOnKeyguard_requestsFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor).requestFaceAuth(true,
+                FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_notDozingAndFaceDetectionIsNotRunning_startsUnlockAnimation() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+        when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mNotificationStackScrollLayoutController).setUnlockHintRunning(true);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_notDozingAndFaceDetectionIsRunning_doesNotStartUnlockHint() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(false, false);
+        when(mUpdateMonitor.requestFaceAuth(true, NOTIFICATION_PANEL_CLICKED)).thenReturn(true);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mNotificationStackScrollLayoutController, never()).setUnlockHintRunning(true);
+    }
+
+    @Test
+    public void onEmptySpaceClicked_whenDozingAndOnKeyguard_doesNotRequestFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(KEYGUARD);
+        mNotificationPanelViewController.setDozing(true, false);
+
+        // This sets the dozing state that is read when onMiddleClicked is eventually invoked.
+        mTouchHandler.onTouch(mock(View.class), mDownMotionEvent);
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+    }
+
+    @Test
+    public void onEmptySpaceClicked_whenStatusBarShadeLocked_doesNotRequestFaceAuth() {
+        StatusBarStateController.StateListener statusBarStateListener =
+                mNotificationPanelViewController.mStatusBarStateListener;
+        statusBarStateListener.onStateChanged(SHADE_LOCKED);
+
+        mEmptySpaceClickListenerCaptor.getValue().onEmptySpaceClicked(0, 0);
+
+        verify(mUpdateMonitor, never()).requestFaceAuth(anyBoolean(), anyString());
+
+    }
 
     /**
      * When shade is flinging to close and this fling is not intercepted,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
index 12ef036..bdafc7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt
@@ -66,6 +66,8 @@
     @Mock
     private lateinit var largeScreenShadeHeaderController: LargeScreenShadeHeaderController
     @Mock
+    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
+    @Mock
     private lateinit var featureFlags: FeatureFlags
     @Captor
     lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
@@ -96,6 +98,7 @@
                 navigationModeController,
                 overviewProxyService,
                 largeScreenShadeHeaderController,
+                shadeExpansionStateManager,
                 featureFlags,
                 delayableExecutor
         )
@@ -380,6 +383,7 @@
                 navigationModeController,
                 overviewProxyService,
                 largeScreenShadeHeaderController,
+                shadeExpansionStateManager,
                 featureFlags,
                 delayableExecutor
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index ad3d3d2..95cf9d6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -88,6 +88,7 @@
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private AuthController mAuthController;
+    @Mock private ShadeExpansionStateManager mShadeExpansionStateManager;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
 
     private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
@@ -103,7 +104,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager, mKeyguardStateController,
-                mScreenOffAnimationController, mAuthController) {
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager) {
                     @Override
                     protected boolean isDebuggable() {
                         return false;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
index eb34561..cc45cf88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/AnimatableClockViewTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.util.mockito.any
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -55,7 +56,7 @@
         clockView.animateAppearOnLockscreen()
         clockView.measure(50, 50)
 
-        verify(mockTextAnimator).glyphFilter = null
+        verify(mockTextAnimator).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, false, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
     }
@@ -66,7 +67,7 @@
         clockView.measure(50, 50)
         clockView.animateAppearOnLockscreen()
 
-        verify(mockTextAnimator, times(2)).glyphFilter = null
+        verify(mockTextAnimator, times(2)).glyphFilter = any()
         verify(mockTextAnimator).setTextStyle(100, -1.0f, 200, false, 0L, null, 0L, null)
         verify(mockTextAnimator).setTextStyle(300, -1.0f, 200, true, 350L, null, 0L, null)
         verifyNoMoreInteractions(mockTextAnimator)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ffb41e5..70cbc64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.drawable.Drawable
 import android.os.Handler
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -104,13 +105,14 @@
             mockContext,
             mockPluginManager,
             mockHandler,
-            fakeDefaultProvider
+            isEnabled = true,
+            userHandle = UserHandle.USER_ALL,
+            defaultClockProvider = fakeDefaultProvider
         ) {
             override var currentClockId: ClockId
                 get() = settingValue
                 set(value) { settingValue = value }
         }
-        registry.isEnabled = true
 
         verify(mockPluginManager)
             .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index cf5fa87..64dc956 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -16,6 +16,11 @@
 
 package com.android.systemui.shared.system;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
@@ -25,11 +30,6 @@
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -40,6 +40,7 @@
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -73,12 +74,12 @@
                 .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */)
                 .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build();
         // Check apps extraction
-        RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
+        RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
                 mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(2, wrapped.length);
         int changeLayer = -1;
         int closeLayer = -1;
-        for (RemoteAnimationTargetCompat t : wrapped) {
+        for (RemoteAnimationTarget t : wrapped) {
             if (t.mode == MODE_CHANGING) {
                 changeLayer = t.prefixOrderIndex;
             } else if (t.mode == MODE_CLOSING) {
@@ -91,14 +92,14 @@
         assertTrue(closeLayer < changeLayer);
 
         // Check wallpaper extraction
-        RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+        RemoteAnimationTarget[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, wallps.length);
         assertTrue(wallps[0].prefixOrderIndex < closeLayer);
         assertEquals(MODE_OPENING, wallps[0].mode);
 
         // Check non-apps extraction
-        RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+        RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, nonApps.length);
         assertTrue(nonApps[0].prefixOrderIndex < closeLayer);
@@ -115,9 +116,9 @@
         change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME));
         change.setEndAbsBounds(endBounds);
         change.setEndRelOffset(0, 0);
-        final RemoteAnimationTargetCompat wrapped = new RemoteAnimationTargetCompat(change,
-                0 /* order */, tinfo, mock(SurfaceControl.Transaction.class));
-        assertEquals(ACTIVITY_TYPE_HOME, wrapped.activityType);
+        RemoteAnimationTarget wrapped = RemoteAnimationTargetCompat.newTarget(
+                change, 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class), null);
+        assertEquals(ACTIVITY_TYPE_HOME, wrapped.windowConfiguration.getActivityType());
         assertEquals(new Rect(0, 0, 100, 140), wrapped.localBounds);
         assertEquals(endBounds, wrapped.screenSpaceBounds);
         assertTrue(wrapped.isTranslucent);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 8643e86..3d11ced 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -10,7 +10,7 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.media.MediaHierarchyManager
+import com.android.systemui.media.controls.ui.MediaHierarchyManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.shade.NotificationPanelViewController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
index 44cbe51..fbb8ebf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.statusbar.notification.row.ExpandableView
 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager
@@ -56,6 +57,7 @@
     private val configurationController: ConfigurationController = mock()
     private val statusBarStateController: StatusBarStateController = mock()
     private val falsingManager: FalsingManager = mock()
+    private val shadeExpansionStateManager: ShadeExpansionStateManager = mock()
     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock()
     private val falsingCollector: FalsingCollector = mock()
     private val dumpManager: DumpManager = mock()
@@ -65,7 +67,8 @@
     fun setUp() {
         whenever(expandableView.collapsedHeight).thenReturn(collapsedHeight)
 
-        pulseExpansionHandler = PulseExpansionHandler(
+        pulseExpansionHandler =
+            PulseExpansionHandler(
                 mContext,
                 wakeUpCoordinator,
                 bypassController,
@@ -74,10 +77,11 @@
                 configurationController,
                 statusBarStateController,
                 falsingManager,
+                shadeExpansionStateManager,
                 lockscreenShadeTransitionController,
                 falsingCollector,
                 dumpManager
-        )
+            )
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
index 4b458f5..dda7fad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
@@ -31,8 +31,8 @@
     private long mCreationTime = 0;
     @Nullable private GroupEntry mParent = GroupEntry.ROOT_ENTRY;
     private NotifSection mNotifSection;
-    private NotificationEntry mSummary = null;
-    private List<NotificationEntry> mChildren = new ArrayList<>();
+    @Nullable private NotificationEntry mSummary = null;
+    private final List<NotificationEntry> mChildren = new ArrayList<>();
 
     /** Builds a new instance of GroupEntry */
     public GroupEntry build() {
@@ -41,7 +41,9 @@
         ge.getAttachState().setSection(mNotifSection);
 
         ge.setSummary(mSummary);
-        mSummary.setParent(ge);
+        if (mSummary != null) {
+            mSummary.setParent(ge);
+        }
 
         for (NotificationEntry child : mChildren) {
             ge.addChild(child);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 851517e..3b05321 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -1498,45 +1498,8 @@
     }
 
     @Test
-    public void testMissingRankingWhenRemovalFeatureIsDisabled() {
+    public void testMissingRanking() {
         // GIVEN a pipeline with one two notifications
-        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false);
-        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
-        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
-        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
-        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
-        clearInvocations(mCollectionListener);
-
-        // GIVEN the message for removing key1 gets does not reach NotifCollection
-        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
-        // WHEN the message for removing key2 arrives
-        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
-
-        // THEN only entry2 gets removed
-        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
-        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
-        verify(mCollectionListener).onRankingApplied();
-        verifyNoMoreInteractions(mCollectionListener);
-        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
-        verify(mLogger, never()).logRecoveredRankings(any(), anyInt());
-        clearInvocations(mCollectionListener, mLogger);
-
-        // WHEN a ranking update includes key1 again
-        mNoMan.setRanking(key1, ranking1);
-        mNoMan.issueRankingUpdate();
-
-        // VERIFY that we do nothing but log the 'recovery'
-        verify(mCollectionListener).onRankingUpdate(any());
-        verify(mCollectionListener).onRankingApplied();
-        verifyNoMoreInteractions(mCollectionListener);
-        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
-        verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0));
-    }
-
-    @Test
-    public void testMissingRankingWhenRemovalFeatureIsEnabled() {
-        // GIVEN a pipeline with one two notifications
-        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true);
         String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
         String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
         NotificationEntry entry1 = mCollectionListener.getEntry(key1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 82e32b2..09f8a10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -34,10 +34,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -135,6 +137,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         allowTestableLooperAsMainThread();
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
 
         mListBuilder = new ShadeListBuilder(
                 mDumpManager,
@@ -1995,22 +1998,89 @@
     }
 
     @Test
-    public void testStableOrdering() {
+    public void testActiveOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering_withLegacyStability() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(false);
         mStabilityManager.setAllowEntryReordering(false);
-        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG"); // Z and X + gap
-        verify(mStabilityManager, times(4)).onEntryReorderSuppressed();
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFXBG", "XABCDEFG", false); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "XZABCDEFG", false); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "XZABCDEFG", false); // Z and X + gap
+    }
+
+    @Test
+    public void testStableOrdering() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        mStabilityManager.setAllowEntryReordering(false);
+        // No input or output
+        assertOrder("", "", "", true);
+        // Remove everything
+        assertOrder("ABCDEFG", "", "", true);
+        // Literally no changes
+        assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
+
+        // No stable order
+        assertOrder("", "ABCDEFG", "ABCDEFG", true);
+
+        // F moved after A, and...
+        assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
+
+        // B moved after F, and...
+        assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
+        assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
+
+        // Swap F and B, and...
+        assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false);   // No other changes
+        assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
+        assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
+        assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
+        assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
+        assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
+        assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
+
+        // Remove a bunch of entries at once
+        assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
+
+        // Remove a bunch of entries and scramble
+        assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
+
+        // Add a bunch of entries at once
+        assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
+        assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
+        assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
+        assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
+        assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
     }
 
     @Test
     public void testActiveOrdering() {
-        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG"); // X
-        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG"); // no change
-        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG"); // Z and X
-        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG"); // Z and X + gap
-        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
+        assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
+        assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
+        assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
     }
 
     @Test
@@ -2062,6 +2132,52 @@
     }
 
     @Test
+    public void stableOrderingDisregardedWithSectionChange() {
+        when(mNotifPipelineFlags.isSemiStableSortEnabled()).thenReturn(true);
+        // GIVEN the first sectioner's packages can be changed from run-to-run
+        List<String> mutableSectionerPackages = new ArrayList<>();
+        mutableSectionerPackages.add(PACKAGE_1);
+        mListBuilder.setSectioners(asList(
+                new PackageSectioner(mutableSectionerPackages, null),
+                new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(4);
+        addNotif(1, PACKAGE_1).setRank(5);
+        addNotif(2, PACKAGE_2).setRank(1);
+        addNotif(3, PACKAGE_2).setRank(2);
+        addNotif(4, PACKAGE_3).setRank(3);
+        dispatchBuild();
+
+        // VERIFY the order and that entry reordering has not been suppressed
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3),
+                notif(4)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the first section now claims PACKAGE_3 notifications
+        mutableSectionerPackages.add(PACKAGE_3);
+        dispatchBuild();
+
+        // VERIFY the re-sectioned notification is inserted at #1 of the first section, which
+        // is the correct position based on its rank, rather than #3 in the new section simply
+        // because it was #3 in its previous section.
+        verifyBuiltList(
+                notif(4),
+                notif(0),
+                notif(1),
+                notif(2),
+                notif(3)
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+    }
+
+    @Test
     public void testStableChildOrdering() {
         // WHEN the list is originally built with reordering disabled
         mStabilityManager.setAllowEntryReordering(false);
@@ -2112,6 +2228,85 @@
         );
     }
 
+    @Test
+    public void groupRevertingToSummaryDoesNotRetainStablePositionWithLegacyIndexLogic() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(false);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary (incorrectly) moves to the top of the section where it is ranked,
+        // despite visual stability being active
+        verifyBuiltList(
+                notif(2),
+                notif(0),
+                notif(1)
+        );
+    }
+
+    @Test
+    public void groupRevertingToSummaryRetainsStablePosition() {
+        when(mNotifPipelineFlags.isStabilityIndexFixEnabled()).thenReturn(true);
+
+        // GIVEN a notification group is on screen
+        mStabilityManager.setAllowEntryReordering(false);
+
+        // WHEN the list is originally built with reordering disabled (and section changes allowed)
+        addNotif(0, PACKAGE_1).setRank(2);
+        addNotif(1, PACKAGE_1).setRank(3);
+        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
+        addGroupChild(3, PACKAGE_1, "group").setRank(5);
+        addGroupChild(4, PACKAGE_1, "group").setRank(6);
+        dispatchBuild();
+
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                group(
+                        summary(2),
+                        child(3),
+                        child(4)
+                )
+        );
+
+        // WHEN the notification summary rank increases and children removed
+        setNewRank(notif(2).entry, 1);
+        mEntrySet.remove(4);
+        mEntrySet.remove(3);
+        dispatchBuild();
+
+        // VERIFY the summary stays in the same location on rebuild
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(2)
+        );
+    }
+
     private static void setNewRank(NotificationEntry entry, int rank) {
         entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
     }
@@ -2255,26 +2450,35 @@
         return addGroupChildWithTag(index, packageId, groupId, null);
     }
 
-    private void assertOrder(String visible, String active, String expected) {
+    private void assertOrder(String visible, String active, String expected,
+            boolean isOrderedCorrectly) {
         StringBuilder differenceSb = new StringBuilder();
+        NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
         for (char c : active.toCharArray()) {
             if (visible.indexOf(c) < 0) differenceSb.append(c);
         }
         String difference = differenceSb.toString();
 
+        int globalIndex = 0;
         for (int i = 0; i < visible.length(); i++) {
-            addNotif(i, String.valueOf(visible.charAt(i)))
-                    .setRank(active.indexOf(visible.charAt(i)))
+            final char c = visible.charAt(i);
+            // Skip notifications which aren't active anymore
+            if (!active.contains(String.valueOf(c))) continue;
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(i);
-
         }
 
-        for (int i = 0; i < difference.length(); i++) {
-            addNotif(i + visible.length(), String.valueOf(difference.charAt(i)))
-                    .setRank(active.indexOf(difference.charAt(i)))
+        for (char c : difference.toCharArray()) {
+            addNotif(globalIndex++, String.valueOf(c))
+                    .setRank(active.indexOf(c))
+                    .setSection(section)
                     .setStableIndex(-1);
         }
 
+        clearInvocations(mStabilityManager);
+
         dispatchBuild();
         StringBuilder resultSb = new StringBuilder();
         for (int i = 0; i < expected.length(); i++) {
@@ -2284,6 +2488,9 @@
         assertEquals("visible [" + visible + "] active [" + active + "]",
                 expected, resultSb.toString());
         mEntrySet.clear();
+
+        verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
+                .onEntryReorderSuppressed();
     }
 
     private int nextId(String packageName) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 340bc96..3ff7639 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -674,7 +674,9 @@
     @Test
     fun testOnRankingApplied_newEntryShouldAlert() {
         // GIVEN that mEntry has never interrupted in the past, and now should
+        // and is new enough to do so
         assertFalse(mEntry.hasInterrupted())
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis())
         setShouldHeadsUp(mEntry)
         whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
 
@@ -690,8 +692,9 @@
 
     @Test
     fun testOnRankingApplied_alreadyAlertedEntryShouldNotAlertAgain() {
-        // GIVEN that mEntry has alerted in the past
+        // GIVEN that mEntry has alerted in the past, even if it's new
         mEntry.setInterruption()
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis())
         setShouldHeadsUp(mEntry)
         whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
 
@@ -725,6 +728,27 @@
         verify(mHeadsUpManager).showNotification(mEntry)
     }
 
+    @Test
+    fun testOnRankingApplied_entryUpdatedButTooOld() {
+        // GIVEN that mEntry is added in a state where it should not HUN
+        setShouldHeadsUp(mEntry, false)
+        mCollectionListener.onEntryAdded(mEntry)
+
+        // and it was actually added 10s ago
+        mCoordinator.setUpdateTime(mEntry, mSystemClock.currentTimeMillis() - 10000)
+
+        // WHEN it is updated to HUN and then a ranking update occurs
+        setShouldHeadsUp(mEntry)
+        whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+        mCollectionListener.onRankingApplied()
+        mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+        mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+        // THEN the notification is never bound or shown
+        verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+        verify(mHeadsUpManager, never()).showNotification(any())
+    }
+
     private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
         whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
         whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
index e1e5051..590c902 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
@@ -35,7 +35,7 @@
 
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index dcf2455..b6b0b77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -261,23 +261,15 @@
         mNotifInflater.invokeInflateCallbackForEntry(mEntry);
 
         // WHEN notification is moved under a parent
-        NotificationEntry groupSummary = getNotificationEntryBuilder()
-                .setParent(ROOT_ENTRY)
-                .setGroupSummary(mContext, true)
-                .setGroup(mContext, TEST_GROUP_KEY)
-                .build();
-        GroupEntry parent = mock(GroupEntry.class);
-        when(parent.getSummary()).thenReturn(groupSummary);
-        NotificationEntryBuilder.setNewParent(mEntry, parent);
-        mCollectionListener.onEntryInit(groupSummary);
-        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry, groupSummary));
+        NotificationEntryBuilder.setNewParent(mEntry, mock(GroupEntry.class));
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry));
 
         // THEN we rebind it as not-minimized
         verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any());
         assertFalse(mParamsCaptor.getValue().isLowPriority());
 
-        // THEN we filter it because the parent summary is not yet inflated.
-        assertTrue(mUninflatedFilter.shouldFilterOut(mEntry, 0));
+        // THEN we do not filter it because it's not the first inflation.
+        assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0));
     }
 
     @Test
@@ -401,6 +393,36 @@
     }
 
     @Test
+    public void testNullGroupSummary() {
+        // GIVEN a newly-posted group with a summary and two children
+        final GroupEntry group = new GroupEntryBuilder()
+                .setCreationTime(400)
+                .setSummary(getNotificationEntryBuilder().setId(1).build())
+                .addChild(getNotificationEntryBuilder().setId(2).build())
+                .addChild(getNotificationEntryBuilder().setId(3).build())
+                .build();
+        fireAddEvents(List.of(group));
+        final NotificationEntry child0 = group.getChildren().get(0);
+        final NotificationEntry child1 = group.getChildren().get(1);
+        mBeforeFilterListener.onBeforeFinalizeFilter(List.of(group));
+
+        // WHEN the summary is pruned
+        new GroupEntryBuilder()
+                .setCreationTime(400)
+                .addChild(child0)
+                .addChild(child1)
+                .build();
+
+        // WHEN all of the children (but not the summary) finish inflating
+        mNotifInflater.invokeInflateCallbackForEntry(child0);
+        mNotifInflater.invokeInflateCallbackForEntry(child1);
+
+        // THEN the entire group is not filtered out
+        assertFalse(mUninflatedFilter.shouldFilterOut(child0, 401));
+        assertFalse(mUninflatedFilter.shouldFilterOut(child1, 401));
+    }
+
+    @Test
     public void testPartiallyInflatedGroupsAreNotFilteredOutIfSummaryReinflate() {
         // GIVEN a newly-posted group with a summary and two children
         final String groupKey = "test_reinflate_group";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
new file mode 100644
index 0000000..1cdd023
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/SemiStableSortTest.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.util.Log
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class SemiStableSortTest : SysuiTestCase() {
+
+    var shuffleInput: Boolean = false
+    var testStabilizeTo: Boolean = false
+    var sorter: SemiStableSort? = null
+
+    @Before
+    fun setUp() {
+        shuffleInput = false
+        sorter = null
+    }
+
+    private fun stringStabilizeTo(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        val result = mutableListOf<Char>()
+        return (sorter ?: SemiStableSort())
+            .stabilizeTo(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                result,
+            )
+            .let { ordered -> result.joinToString("") to ordered }
+    }
+
+    private fun stringSort(
+        stableOrder: String,
+        activeOrder: String,
+    ): Pair<String, Boolean> {
+        val actives = activeOrder.toMutableList()
+        if (shuffleInput) {
+            actives.shuffle()
+        }
+        return (sorter ?: SemiStableSort())
+            .sort(
+                actives,
+                { ch -> stableOrder.indexOf(ch).takeIf { it >= 0 } },
+                compareBy { activeOrder.indexOf(it) },
+            )
+            .let { ordered -> actives.joinToString("") to ordered }
+    }
+
+    private fun testCase(
+        stableOrder: String,
+        activeOrder: String,
+        expected: String,
+        expectOrdered: Boolean,
+    ) {
+        val (mergeResult, ordered) =
+            if (testStabilizeTo) stringStabilizeTo(stableOrder, activeOrder)
+            else stringSort(stableOrder, activeOrder)
+        val resultPass = expected == mergeResult
+        val orderedPass = ordered == expectOrdered
+        val pass = resultPass && orderedPass
+        val resultSuffix =
+            if (resultPass) "result=$expected" else "expected=$expected got=$mergeResult"
+        val orderedSuffix =
+            if (orderedPass) "ordered=$ordered" else "expected ordered to be $expectOrdered"
+        val readableResult = "stable=$stableOrder active=$activeOrder $resultSuffix $orderedSuffix"
+        Log.d("SemiStableSortTest", "${if (pass) "PASS" else "FAIL"}: $readableResult")
+        if (!pass) {
+            throw AssertionError("Test case failed: $readableResult")
+        }
+    }
+
+    private fun runAllTestCases() {
+        // No input or output
+        testCase("", "", "", true)
+        // Remove everything
+        testCase("ABCDEFG", "", "", true)
+        // Literally no changes
+        testCase("ABCDEFG", "ABCDEFG", "ABCDEFG", true)
+
+        // No stable order
+        testCase("", "ABCDEFG", "ABCDEFG", true)
+
+        // F moved after A, and...
+        testCase("ABCDEFG", "AFBCDEG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false) // Insert X where F was
+
+        // B moved after F, and...
+        testCase("ABCDEFG", "ACDEFBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false) // Insert X after B
+        testCase("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false) // Insert X where B was
+
+        // Swap F and B, and...
+        testCase("ABCDEFG", "AFCDEBG", "ABCDEFG", false) // No other changes
+        testCase("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false) // Insert X before F
+        testCase("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false) // Insert X after F
+        testCase("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false) // Insert X between CD (Alt: ABCXDEFG)
+        testCase("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false) // Insert X between DE (Alt: ABCDEFXG)
+        testCase("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false) // Insert X before B
+        testCase("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false) // Insert X after B
+
+        // Remove a bunch of entries at once
+        testCase("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true)
+
+        // Remove a bunch of entries and scramble
+        testCase("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false)
+
+        // Add a bunch of entries at once
+        testCase("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true)
+
+        // Add a bunch of entries and reverse originals
+        // NOTE: Some of these don't have obviously correct answers
+        testCase("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false) // appended
+        testCase("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false) // prepended
+        testCase("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false) // closer to back: append
+        testCase("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false) // closer to front: prepend
+        testCase("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false) // split new entries
+
+        // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
+        testCase("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false)
+    }
+
+    @Test
+    fun testSort() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithSingleInstance() {
+        testStabilizeTo = false
+        shuffleInput = false
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testSortWithShuffledInput() {
+        testStabilizeTo = false
+        shuffleInput = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeTo() {
+        testStabilizeTo = true
+        sorter = null
+        runAllTestCases()
+    }
+
+    @Test
+    fun testStabilizeToWithSingleInstance() {
+        testStabilizeTo = true
+        sorter = SemiStableSort()
+        runAllTestCases()
+    }
+
+    @Test
+    fun testIsSorted() {
+        val intCmp = Comparator<Int> { x, y -> Integer.compare(x, y) }
+        SemiStableSort.apply {
+            assertTrue(emptyList<Int>().isSorted(intCmp))
+            assertTrue(listOf(1).isSorted(intCmp))
+            assertTrue(listOf(1, 2).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4).isSorted(intCmp))
+            assertTrue(listOf(1, 2, 3, 4, 5).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 1, 1, 1).isSorted(intCmp))
+            assertTrue(listOf(1, 1, 2, 2, 3, 3).isSorted(intCmp))
+            assertFalse(listOf(2, 1).isSorted(intCmp))
+            assertFalse(listOf(2, 1, 2).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 1).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 2, 5).isSorted(intCmp))
+            assertFalse(listOf(5, 2, 3, 4, 5).isSorted(intCmp))
+            assertFalse(listOf(1, 2, 3, 4, 1).isSorted(intCmp))
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
new file mode 100644
index 0000000..2036954
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderHelperTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper.getContiguousSubLists
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class ShadeListBuilderHelperTest : SysuiTestCase() {
+
+    @Test
+    fun testGetContiguousSubLists() {
+        assertThat(getContiguousSubLists("AAAAAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A', 'A', 'A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBB".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABAA".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('A', 'A'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList()) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('D'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+        assertThat(getContiguousSubLists("AAABBBBCCDEEE".toList(), minLength = 2) { it })
+            .containsExactly(
+                listOf('A', 'A', 'A'),
+                listOf('B', 'B', 'B', 'B'),
+                listOf('C', 'C'),
+                listOf('E', 'E', 'E'),
+            )
+            .inOrder()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 46f630b..ea311da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -51,12 +51,14 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.NotificationInterruptEvent;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -97,6 +99,7 @@
     NotifPipelineFlags mFlags;
     @Mock
     KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    UiEventLoggerFake mUiEventLoggerFake;
     @Mock
     PendingIntent mPendingIntent;
 
@@ -107,6 +110,8 @@
         MockitoAnnotations.initMocks(this);
         when(mFlags.fullScreenIntentRequiresKeyguard()).thenReturn(false);
 
+        mUiEventLoggerFake = new UiEventLoggerFake();
+
         mNotifInterruptionStateProvider =
                 new NotificationInterruptStateProviderImpl(
                         mContext.getContentResolver(),
@@ -120,7 +125,8 @@
                         mLogger,
                         mMockHandler,
                         mFlags,
-                        mKeyguardNotificationVisibilityProvider);
+                        mKeyguardNotificationVisibilityProvider,
+                        mUiEventLoggerFake);
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
@@ -442,6 +448,13 @@
         verify(mLogger, never()).logNoFullscreen(any(), any());
         verify(mLogger).logNoFullscreenWarning(entry, "GroupAlertBehavior will prevent HUN");
         verify(mLogger, never()).logFullscreen(any(), any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.FSI_SUPPRESSED_SUPPRESSIVE_GROUP_ALERT_BEHAVIOR.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     @Test
@@ -600,6 +613,13 @@
         verify(mLogger, never()).logNoFullscreen(any(), any());
         verify(mLogger).logNoFullscreenWarning(entry, "Expected not to HUN while not on keyguard");
         verify(mLogger, never()).logFullscreen(any(), any());
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1);
+        UiEventLoggerFake.FakeUiEvent fakeUiEvent = mUiEventLoggerFake.get(0);
+        assertThat(fakeUiEvent.eventId).isEqualTo(
+                NotificationInterruptEvent.FSI_SUPPRESSED_NO_HUN_OR_KEYGUARD.getId());
+        assertThat(fakeUiEvent.uid).isEqualTo(entry.getSbn().getUid());
+        assertThat(fakeUiEvent.packageName).isEqualTo(entry.getSbn().getPackageName());
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
index 16e2441..f69839b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt
@@ -28,30 +28,21 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.notification.NotificationUtils
-import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-class NotificationMemoryMonitorTest : SysuiTestCase() {
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
+class NotificationMemoryMeterTest : SysuiTestCase() {
 
     @Test
     fun currentNotificationMemoryUse_plainNotification() {
         val notification = createBasicNotification().build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -69,8 +60,8 @@
     fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
         val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
         val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -92,8 +83,8 @@
                     RemoteViews(context.packageName, android.R.layout.list_content)
                 )
                 .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -112,8 +103,8 @@
         val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
         val notification =
             createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = 444444,
@@ -141,8 +132,8 @@
                         .bigLargeIcon(bigPictureIcon)
                 )
                 .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -167,8 +158,8 @@
             createBasicNotification()
                 .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
                 .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -203,8 +194,8 @@
                         .addHistoricMessage(historicMessage)
                 )
                 .build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -225,8 +216,8 @@
         val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
         val extender = Notification.CarExtender().setLargeIcon(carIcon)
         val notification = createBasicNotification().extend(extender).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -246,8 +237,8 @@
         val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
         val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
         val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
-        val nmm = createNMMWithNotifications(listOf(notification))
-        val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
+        val memoryUse =
+            NotificationMemoryMeter.notificationMemoryUse(createNotificationEntry(notification))
         assertNotificationObjectSizes(
             memoryUse = memoryUse,
             smallIcon = notification.smallIcon.bitmap.allocationByteCount,
@@ -283,10 +274,10 @@
         extender: Int,
         style: String?,
         styleIcon: Int,
-        hasCustomView: Boolean
+        hasCustomView: Boolean,
     ) {
         assertThat(memoryUse.packageName).isEqualTo("test_pkg")
-        assertThat(memoryUse.notificationId)
+        assertThat(memoryUse.notificationKey)
             .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
         assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
         assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
@@ -301,21 +292,14 @@
     }
 
     private fun getUseObject(
-        singleItemUseList: List<NotificationMemoryUsage>
+        singleItemUseList: List<NotificationMemoryUsage>,
     ): NotificationMemoryUsage {
         assertThat(singleItemUseList).hasSize(1)
         return singleItemUseList[0]
     }
 
-    private fun createNMMWithNotifications(
-        notifications: List<Notification>
-    ): NotificationMemoryMonitor {
-        val notifPipeline: NotifPipeline = mock()
-        val notificationEntries =
-            notifications.map { n ->
-                NotificationEntryBuilder().setTag("test").setNotification(n).build()
-            }
-        whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
-        return NotificationMemoryMonitor(notifPipeline, mock())
-    }
+    private fun createNotificationEntry(
+        notification: Notification,
+    ): NotificationEntry =
+        NotificationEntryBuilder().setTag("test").setNotification(notification).build()
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
new file mode 100644
index 0000000..3a16fb3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt
@@ -0,0 +1,148 @@
+package com.android.systemui.statusbar.notification.logging
+
+import android.app.Notification
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.widget.RemoteViews
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.tests.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationMemoryViewWalkerTest : SysuiTestCase() {
+
+    private lateinit var testHelper: NotificationTestHelper
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        testHelper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+    }
+
+    @Test
+    fun testViewWalker_nullRow_returnsEmptyView() {
+        val result = NotificationMemoryViewWalker.getViewUsage(null)
+        assertThat(result).isNotNull()
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun testViewWalker_plainNotification() {
+        val row = testHelper.createRow()
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(5)
+        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0))
+        assertThat(result)
+            .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun testViewWalker_bigPictureNotification() {
+        val bigPicture = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
+        val largeIcon = Icon.createWithBitmap(Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888))
+        val row =
+            testHelper.createRow(
+                Notification.Builder(mContext)
+                    .setContentText("Test")
+                    .setContentTitle("title")
+                    .setSmallIcon(icon)
+                    .setLargeIcon(largeIcon)
+                    .setStyle(Notification.BigPictureStyle().bigPicture(bigPicture))
+                    .build()
+            )
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(5)
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_EXPANDED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    largeIcon.bitmap.allocationByteCount,
+                    0,
+                    bigPicture.allocationByteCount,
+                    0,
+                    bigPicture.allocationByteCount +
+                        icon.bitmap.allocationByteCount +
+                        largeIcon.bitmap.allocationByteCount
+                )
+            )
+
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_CONTRACTED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    largeIcon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount
+                )
+            )
+        // Due to deduplication, this should all be 0.
+        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+    }
+
+    @Test
+    fun testViewWalker_customView() {
+        val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888))
+        val bitmap = Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)
+
+        val views = RemoteViews(mContext.packageName, R.layout.custom_view_dark)
+        views.setImageViewBitmap(R.id.custom_view_dark_image, bitmap)
+        val row =
+            testHelper.createRow(
+                Notification.Builder(mContext)
+                    .setContentText("Test")
+                    .setContentTitle("title")
+                    .setSmallIcon(icon)
+                    .setCustomContentView(views)
+                    .setCustomBigContentView(views)
+                    .build()
+            )
+        val result = NotificationMemoryViewWalker.getViewUsage(row)
+        assertThat(result).hasSize(5)
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_CONTRACTED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    bitmap.allocationByteCount,
+                    bitmap.allocationByteCount + icon.bitmap.allocationByteCount
+                )
+            )
+        assertThat(result)
+            .contains(
+                NotificationViewUsage(
+                    ViewType.PRIVATE_EXPANDED_VIEW,
+                    icon.bitmap.allocationByteCount,
+                    0,
+                    0,
+                    0,
+                    bitmap.allocationByteCount,
+                    bitmap.allocationByteCount + icon.bitmap.allocationByteCount
+                )
+            )
+        // Due to deduplication, this should all be 0.
+        assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 8375e7c..5394d88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -51,7 +51,7 @@
 import androidx.test.filters.Suppress;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
deleted file mode 100644
index 81b8e98..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2014 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 static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.view.NotificationHeaderView;
-import android.view.View;
-import android.view.ViewPropertyAnimator;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.internal.widget.NotificationActionListLayout;
-import com.android.internal.widget.NotificationExpandButton;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.statusbar.notification.FeedbackIcon;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class NotificationContentViewTest extends SysuiTestCase {
-
-    NotificationContentView mView;
-
-    @Before
-    @UiThreadTest
-    public void setup() {
-        mDependency.injectMockDependency(MediaOutputDialogFactory.class);
-
-        mView = new NotificationContentView(mContext, null);
-        ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null);
-        ExpandableNotificationRow mockRow = spy(row);
-        doReturn(10).when(mockRow).getIntrinsicHeight();
-
-        mView.setContainingNotification(mockRow);
-        mView.setHeights(10, 20, 30);
-
-        mView.setContractedChild(createViewWithHeight(10));
-        mView.setExpandedChild(createViewWithHeight(20));
-        mView.setHeadsUpChild(createViewWithHeight(30));
-
-        mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
-    }
-
-    private View createViewWithHeight(int height) {
-        View view = new View(mContext, null);
-        view.setMinimumHeight(height);
-        return view;
-    }
-
-    @Test
-    @UiThreadTest
-    public void testSetFeedbackIcon() {
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockContracted);
-        when(mockContracted.getContext()).thenReturn(mContext);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockExpanded);
-        when(mockExpanded.getContext()).thenReturn(mContext);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockHeadsUp);
-        when(mockHeadsUp.getContext()).thenReturn(mContext);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setFeedbackIcon(new FeedbackIcon(R.drawable.ic_feedback_alerted,
-                R.string.notification_feedback_indicator_alerted));
-
-        verify(mockContracted, times(1)).setVisibility(View.VISIBLE);
-        verify(mockExpanded, times(1)).setVisibility(View.VISIBLE);
-        verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE);
-    }
-
-    @Test
-    @UiThreadTest
-    public void testExpandButtonFocusIsCalled() {
-        View mockContractedEB = mock(NotificationExpandButton.class);
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockContractedEB);
-        when(mockContracted.getContext()).thenReturn(mContext);
-
-        View mockExpandedEB = mock(NotificationExpandButton.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockExpandedEB);
-        when(mockExpanded.getContext()).thenReturn(mContext);
-
-        View mockHeadsUpEB = mock(NotificationExpandButton.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockHeadsUpEB);
-        when(mockHeadsUp.getContext()).thenReturn(mContext);
-
-        // Set up all 3 child forms
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        // This is required to call requestAccessibilityFocus()
-        mView.setFocusOnVisibilityChange();
-
-        // The following will initialize the view and switch from not visible to expanded.
-        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
-        mView.setHeadsUp(true);
-
-        verify(mockContractedEB, times(0)).requestAccessibilityFocus();
-        verify(mockExpandedEB, times(1)).requestAccessibilityFocus();
-        verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus();
-    }
-
-    @Test
-    @UiThreadTest
-    public void testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
-        View mockContracted = mock(NotificationHeaderView.class);
-
-        View mockExpandedActions = mock(NotificationActionListLayout.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockExpandedActions);
-
-        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockHeadsUpActions);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setRemoteInputVisible(true);
-
-        verify(mockContracted, times(0)).findViewById(0);
-        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-    }
-
-    @Test
-    @UiThreadTest
-    public void testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
-        View mockContracted = mock(NotificationHeaderView.class);
-
-        View mockExpandedActions = mock(NotificationActionListLayout.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockExpandedActions);
-
-        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockHeadsUpActions);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setRemoteInputVisible(false);
-
-        verify(mockContracted, times(0)).findViewById(0);
-        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
new file mode 100644
index 0000000..562b4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.res.Resources
+import android.os.UserHandle
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.view.NotificationHeaderView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.widget.NotificationActionListLayout
+import com.android.internal.widget.NotificationExpandButton
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.statusbar.notification.FeedbackIcon
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationContentViewTest : SysuiTestCase() {
+    private lateinit var view: NotificationContentView
+
+    @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
+
+    private val notificationContentMargin =
+        mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin)
+
+    @Before
+    fun setup() {
+        initMocks(this)
+
+        mDependency.injectMockDependency(MediaOutputDialogFactory::class.java)
+
+        view = spy(NotificationContentView(mContext, /* attrs= */ null))
+        val row = ExpandableNotificationRow(mContext, /* attrs= */ null)
+        row.entry = createMockNotificationEntry(false)
+        val spyRow = spy(row)
+        doReturn(10).whenever(spyRow).intrinsicHeight
+
+        with(view) {
+            initialize(mPeopleNotificationIdentifier, mock(), mock(), mock())
+            setContainingNotification(spyRow)
+            setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30)
+            contractedChild = createViewWithHeight(10)
+            expandedChild = createViewWithHeight(20)
+            headsUpChild = createViewWithHeight(30)
+            measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+            layout(0, 0, view.measuredWidth, view.measuredHeight)
+        }
+    }
+
+    private fun createViewWithHeight(height: Int) =
+        View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+
+    @Test
+    fun testSetFeedbackIcon() {
+        // Given: contractedChild, enpandedChild, and headsUpChild being set
+        val mockContracted = createMockNotificationHeaderView()
+        val mockExpanded = createMockNotificationHeaderView()
+        val mockHeadsUp = createMockNotificationHeaderView()
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        // When: FeedBackIcon is set
+        view.setFeedbackIcon(
+            FeedbackIcon(
+                R.drawable.ic_feedback_alerted,
+                R.string.notification_feedback_indicator_alerted
+            )
+        )
+
+        // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible
+        verify(mockContracted).visibility = View.VISIBLE
+        verify(mockExpanded).visibility = View.VISIBLE
+        verify(mockHeadsUp).visibility = View.VISIBLE
+    }
+
+    private fun createMockNotificationHeaderView() =
+        mock<NotificationHeaderView>().apply {
+            whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this)
+            whenever(this.context).thenReturn(mContext)
+        }
+
+    @Test
+    fun testExpandButtonFocusIsCalled() {
+        val mockContractedEB = mock<NotificationExpandButton>()
+        val mockContracted = createMockNotificationHeaderView(mockContractedEB)
+
+        val mockExpandedEB = mock<NotificationExpandButton>()
+        val mockExpanded = createMockNotificationHeaderView(mockExpandedEB)
+
+        val mockHeadsUpEB = mock<NotificationExpandButton>()
+        val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB)
+
+        // Set up all 3 child forms
+        view.contractedChild = mockContracted
+        view.expandedChild = mockExpanded
+        view.headsUpChild = mockHeadsUp
+
+        // This is required to call requestAccessibilityFocus()
+        view.setFocusOnVisibilityChange()
+
+        // The following will initialize the view and switch from not visible to expanded.
+        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
+        view.setHeadsUp(true)
+        verify(mockContractedEB, never()).requestAccessibilityFocus()
+        verify(mockExpandedEB).requestAccessibilityFocus()
+        verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
+    }
+
+    private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) =
+        mock<NotificationHeaderView>().apply {
+            whenever(this.animate()).thenReturn(mock())
+            whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+            whenever(this.context).thenReturn(mContext)
+        }
+
+    @Test
+    fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
+        val mockContracted = mock<NotificationHeaderView>()
+
+        val mockExpandedActions = mock<NotificationActionListLayout>()
+        val mockExpanded = mock<NotificationHeaderView>()
+        whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+        val mockHeadsUpActions = mock<NotificationActionListLayout>()
+        val mockHeadsUp = mock<NotificationHeaderView>()
+        whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        view.setRemoteInputVisible(true)
+
+        verify(mockContracted, never()).findViewById<View>(0)
+        verify(mockExpandedActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+        verify(mockHeadsUpActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+    }
+
+    @Test
+    fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
+        val mockContracted = mock<NotificationHeaderView>()
+
+        val mockExpandedActions = mock<NotificationActionListLayout>()
+        val mockExpanded = mock<NotificationHeaderView>()
+        whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+        val mockHeadsUpActions = mock<NotificationActionListLayout>()
+        val mockHeadsUp = mock<NotificationHeaderView>()
+        whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        view.setRemoteInputVisible(false)
+
+        verify(mockContracted, never()).findViewById<View>(0)
+        verify(mockExpandedActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+        verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+    }
+
+    @Test
+    fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        // Bubble button should not be shown for the given NotificationEntry
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+
+        // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+        view.expandedChild = mockExpandedChild
+
+        // Then: bottom margin of actionListMarginTarget should not change,
+        // still be notificationContentMargin
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        // Bubble button should be shown for the given NotificationEntry
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+
+        // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+        view.expandedChild = mockExpandedChild
+
+        // Then: bottom margin of actionListMarginTarget should be set to 0
+        assertEquals(0, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+        view.expandedChild = mockExpandedChild
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+        // When: call NotificationContentView.onNotificationUpdated() to update the
+        // NotificationEntry, which should not show bubble button
+        view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false))
+
+        // Then: bottom margin of actionListMarginTarget should not change, still be 20
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+        view.expandedChild = mockExpandedChild
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+        // When: call NotificationContentView.onNotificationUpdated() to update the
+        // NotificationEntry, which should show bubble button
+        view.onNotificationUpdated(createMockNotificationEntry(true))
+
+        // Then: bottom margin of actionListMarginTarget should not change, still be 20
+        assertEquals(0, getMarginBottom(actionListMarginTarget))
+    }
+
+    private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.entry).thenReturn(notificationEntry)
+            whenever(this.context).thenReturn(mContext)
+            whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
+        }
+
+    private fun createMockNotificationEntry(showButton: Boolean) =
+        mock<NotificationEntry>().apply {
+            whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
+                .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
+            whenever(this.bubbleMetadata).thenReturn(mock())
+            val sbnMock: StatusBarNotification = mock()
+            val userMock: UserHandle = mock()
+            whenever(this.sbn).thenReturn(sbnMock)
+            whenever(sbnMock.user).thenReturn(userMock)
+            doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
+        }
+
+    private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
+        val outerLayout = LinearLayout(mContext)
+        val innerLayout = LinearLayout(mContext)
+        outerLayout.addView(innerLayout)
+        val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams
+        mlp.setMargins(0, 0, 0, bottomMargin)
+        return innerLayout
+    }
+
+    private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
+            whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
+            whenever(this.entry).thenReturn(notificationEntry)
+            whenever(this.context).thenReturn(mContext)
+
+            val resourcesMock: Resources = mock()
+            whenever(resourcesMock.configuration).thenReturn(mock())
+            whenever(this.resources).thenReturn(resourcesMock)
+        }
+
+    private fun getMarginBottom(layout: LinearLayout): Int =
+        (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index cc4cbbf..e7a435e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -52,7 +52,7 @@
 import com.android.systemui.TestableDependency;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.media.controls.util.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationMediaManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index a95a49c..8c8b644 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -147,8 +147,8 @@
                 createSection(mFirst, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -170,13 +170,13 @@
         when(testHelper.getStatusBarStateController().isDozing()).thenReturn(true);
         row.setHeadsUp(true);
         mRoundnessManager.updateView(entry.getRow(), false);
-        Assert.assertEquals(1f, row.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1f, row.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1f, row.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1f, row.getTopRoundness(), 0.0f);
 
         row.setHeadsUp(false);
         mRoundnessManager.updateView(entry.getRow(), false);
-        Assert.assertEquals(mSmallRadiusRatio, row.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, row.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, row.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, row.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -185,8 +185,8 @@
                 createSection(mFirst, mFirst),
                 createSection(null, mSecond)
         });
-        Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -195,8 +195,8 @@
                 createSection(mFirst, mFirst),
                 createSection(mSecond, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -205,8 +205,8 @@
                 createSection(mFirst, null),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -215,8 +215,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -226,8 +226,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -238,8 +238,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -250,8 +250,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -262,8 +262,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -274,8 +274,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -286,8 +286,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(0.5f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(0.5f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(0.5f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(0.5f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -298,8 +298,8 @@
                 createSection(null, null)
         });
         mFirst.setHeadsUpAnimatingAway(true);
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
 
@@ -312,8 +312,8 @@
         });
         mFirst.setHeadsUpAnimatingAway(true);
         mFirst.setHeadsUpAnimatingAway(false);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index 9d848e8..ecc0224 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -30,7 +30,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 1c9b0be..90061b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -46,7 +46,7 @@
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.KeyguardMediaController;
+import com.android.systemui.media.controls.ui.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -129,6 +129,7 @@
     @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     @Mock private ShadeTransitionController mShadeTransitionController;
     @Mock private FeatureFlags mFeatureFlags;
+    @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
@@ -177,7 +178,8 @@
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
-                mFeatureFlags
+                mFeatureFlags,
+                mNotificationTargetsHelper
         );
 
         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 35c8b61..91aecd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -163,7 +163,7 @@
         mStackScroller.setCentralSurfaces(mCentralSurfaces);
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
-        when(mStackScrollLayoutController.getNoticationRoundessManager())
+        when(mStackScrollLayoutController.getNotificationRoundnessManager())
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
new file mode 100644
index 0000000..a2e9230
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -0,0 +1,107 @@
+package com.android.systemui.statusbar.notification.stack
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.mock
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for {@link NotificationTargetsHelper}. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationTargetsHelperTest : SysuiTestCase() {
+    lateinit var notificationTestHelper: NotificationTestHelper
+    private val sectionsManager: NotificationSectionsManager = mock()
+    private val stackScrollLayout: NotificationStackScrollLayout = mock()
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        notificationTestHelper =
+            NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+    }
+
+    private fun notificationTargetsHelper(
+        notificationGroupCorner: Boolean = true,
+    ) =
+        NotificationTargetsHelper(
+            FakeFeatureFlags().apply {
+                set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner)
+            }
+        )
+
+    @Test
+    fun targetsForFirstNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[0]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.notificationHeaderWrapper, // group header
+                swiped = swiped,
+                after = children.attachedChildren[1],
+            )
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun targetsForMiddleNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[1]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.attachedChildren[0],
+                swiped = swiped,
+                after = children.attachedChildren[2],
+            )
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun targetsForLastNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[2]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.attachedChildren[1],
+                swiped = swiped,
+                after = null,
+            )
+        assertEquals(expected, actual)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index ad497a2..6de8bd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -80,6 +80,7 @@
 
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.jank.InteractionJankMonitor;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
@@ -98,7 +99,8 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -271,7 +273,6 @@
     @Mock private OngoingCallController mOngoingCallController;
     @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
     @Mock private LockscreenShadeTransitionController mLockscreenTransitionController;
-    @Mock private FeatureFlags mFeatureFlags;
     @Mock private NotificationVisibilityProvider mVisibilityProvider;
     @Mock private WallpaperManager mWallpaperManager;
     @Mock private IWallpaperManager mIWallpaperManager;
@@ -296,9 +297,10 @@
 
     private ShadeController mShadeController;
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
-    private FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
-    private FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
-    private InitController mInitController = new InitController();
+    private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
+    private final FakeExecutor mUiBgExecutor = new FakeExecutor(mFakeSystemClock);
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    private final InitController mInitController = new InitController();
     private final DumpManager mDumpManager = new DumpManager();
 
     @Before
@@ -322,7 +324,8 @@
                         mock(NotificationInterruptLogger.class),
                         new Handler(TestableLooper.get(this).getLooper()),
                         mock(NotifPipelineFlags.class),
-                        mock(KeyguardNotificationVisibilityProvider.class));
+                        mock(KeyguardNotificationVisibilityProvider.class),
+                        mock(UiEventLogger.class));
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));
@@ -1017,6 +1020,60 @@
     }
 
     @Test
+    public void collapseShade_callsAnimateCollapsePanels_whenExpanded() {
+        // GIVEN the shade is expanded
+        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+
+        // WHEN collapseShade is called
+        mCentralSurfaces.collapseShade();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShade_doesNotCallAnimateCollapsePanels_whenCollapsed() {
+        // GIVEN the shade is collapsed
+        mCentralSurfaces.setPanelExpanded(false);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+
+        // WHEN collapseShade is called
+        mCentralSurfaces.collapseShade();
+
+        // VERIFY that animateCollapsePanels is NOT called
+        verify(mShadeController, never()).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShadeForBugReport_callsAnimateCollapsePanels_whenFlagDisabled() {
+        // GIVEN the shade is expanded & flag enabled
+        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+        mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, false);
+
+        // WHEN collapseShadeForBugreport is called
+        mCentralSurfaces.collapseShadeForBugreport();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController).animateCollapsePanels();
+    }
+
+    @Test
+    public void collapseShadeForBugReport_doesNotCallAnimateCollapsePanels_whenFlagEnabled() {
+        // GIVEN the shade is expanded & flag enabled
+        mCentralSurfaces.setPanelExpanded(true);
+        mCentralSurfaces.setBarStateForTest(StatusBarState.SHADE);
+        mFeatureFlags.set(Flags.LEAVE_SHADE_OPEN_FOR_BUGREPORT, true);
+
+        // WHEN collapseShadeForBugreport is called
+        mCentralSurfaces.collapseShadeForBugreport();
+
+        // VERIFY that animateCollapsePanels is called
+        verify(mShadeController, never()).animateCollapsePanels();
+    }
+
+    @Test
     public void deviceStateChange_unfolded_shadeOpen_setsLeaveOpenOnKeyguardHide() {
         when(mKeyguardStateController.isShowing()).thenReturn(false);
         setFoldedStates(FOLD_STATE_FOLDED);
@@ -1102,7 +1159,8 @@
                 NotificationInterruptLogger logger,
                 Handler mainHandler,
                 NotifPipelineFlags flags,
-                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+                UiEventLogger uiEventLogger) {
             super(
                     contentResolver,
                     powerManager,
@@ -1115,7 +1173,8 @@
                     logger,
                     mainHandler,
                     flags,
-                    keyguardNotificationVisibilityProvider
+                    keyguardNotificationVisibilityProvider,
+                    uiEventLogger
             );
             mUseHeadsUp = true;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 8da8d04..0c35659 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -117,7 +117,6 @@
     @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor;
     @Mock private BouncerInteractor mBouncerInteractor;
     @Mock private BouncerView mBouncerView;
-//    @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference;
     @Mock private BouncerViewDelegate mBouncerViewDelegate;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
index bf43238..eba3b04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
@@ -20,7 +20,6 @@
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
@@ -34,8 +33,8 @@
 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.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -91,7 +90,7 @@
     fun testStartActivity() {
         `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(false)
         statusBarUserSwitcherContainer.callOnClick()
-        verify(userSwitcherDialogController).showDialog(any(View::class.java))
+        verify(userSwitcherDialogController).showDialog(any(), any())
         `when`(featureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)).thenReturn(true)
         statusBarUserSwitcherContainer.callOnClick()
         verify(activityStarter).startActivity(any(Intent::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt
new file mode 100644
index 0000000..b7a6c01
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.data.repository
+
+import android.os.Handler
+import android.os.Looper
+import android.os.UserHandle
+import android.provider.Settings.Global
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class AirplaneModeRepositoryImplTest : SysuiTestCase() {
+
+    private lateinit var underTest: AirplaneModeRepositoryImpl
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    private lateinit var bgHandler: Handler
+    private lateinit var scope: CoroutineScope
+    private lateinit var settings: FakeSettings
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        bgHandler = Handler(Looper.getMainLooper())
+        scope = CoroutineScope(IMMEDIATE)
+        settings = FakeSettings()
+        settings.userId = UserHandle.USER_ALL
+
+        underTest =
+            AirplaneModeRepositoryImpl(
+                bgHandler,
+                settings,
+                logger,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun isAirplaneMode_initiallyGetsSettingsValue() =
+        runBlocking(IMMEDIATE) {
+            settings.putInt(Global.AIRPLANE_MODE_ON, 1)
+
+            underTest =
+                AirplaneModeRepositoryImpl(
+                    bgHandler,
+                    settings,
+                    logger,
+                    scope,
+                )
+
+            val job = underTest.isAirplaneMode.launchIn(this)
+
+            assertThat(underTest.isAirplaneMode.value).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isAirplaneMode_settingUpdated_valueUpdated() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.isAirplaneMode.launchIn(this)
+
+            settings.putInt(Global.AIRPLANE_MODE_ON, 0)
+            yield()
+            assertThat(underTest.isAirplaneMode.value).isFalse()
+
+            settings.putInt(Global.AIRPLANE_MODE_ON, 1)
+            yield()
+            assertThat(underTest.isAirplaneMode.value).isTrue()
+
+            settings.putInt(Global.AIRPLANE_MODE_ON, 0)
+            yield()
+            assertThat(underTest.isAirplaneMode.value).isFalse()
+
+            job.cancel()
+        }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt
new file mode 100644
index 0000000..63bbdfc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeAirplaneModeRepository : AirplaneModeRepository {
+    private val _isAirplaneMode = MutableStateFlow(false)
+    override val isAirplaneMode: StateFlow<Boolean> = _isAirplaneMode
+
+    fun setIsAirplaneMode(isAirplaneMode: Boolean) {
+        _isAirplaneMode.value = isAirplaneMode
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt
new file mode 100644
index 0000000..33a80e1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+class AirplaneModeInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: AirplaneModeInteractor
+
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+    private lateinit var connectivityRepository: FakeConnectivityRepository
+
+    @Before
+    fun setUp() {
+        airplaneModeRepository = FakeAirplaneModeRepository()
+        connectivityRepository = FakeConnectivityRepository()
+        underTest = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository)
+    }
+
+    @Test
+    fun isAirplaneMode_matchesRepo() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneMode.onEach { latest = it }.launchIn(this)
+
+            airplaneModeRepository.setIsAirplaneMode(true)
+            yield()
+            assertThat(latest).isTrue()
+
+            airplaneModeRepository.setIsAirplaneMode(false)
+            yield()
+            assertThat(latest).isFalse()
+
+            airplaneModeRepository.setIsAirplaneMode(true)
+            yield()
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isForceHidden_repoHasWifiHidden_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
+
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt
new file mode 100644
index 0000000..76016a1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+class AirplaneModeViewModelTest : SysuiTestCase() {
+
+    private lateinit var underTest: AirplaneModeViewModel
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+    private lateinit var connectivityRepository: FakeConnectivityRepository
+    private lateinit var interactor: AirplaneModeInteractor
+    private lateinit var scope: CoroutineScope
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
+        connectivityRepository = FakeConnectivityRepository()
+        interactor = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository)
+        scope = CoroutineScope(IMMEDIATE)
+
+        underTest =
+            AirplaneModeViewModel(
+                interactor,
+                logger,
+                scope,
+            )
+    }
+
+    @Test
+    fun isAirplaneModeIconVisible_notAirplaneMode_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
+            airplaneModeRepository.setIsAirplaneMode(false)
+
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isAirplaneModeIconVisible_forceHidden_outputsFalse() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isAirplaneModeIconVisible_isAirplaneModeAndNotForceHidden_outputsTrue() =
+        runBlocking(IMMEDIATE) {
+            connectivityRepository.setForceHiddenIcons(setOf())
+            airplaneModeRepository.setIsAirplaneMode(true)
+
+            var latest: Boolean? = null
+            val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
new file mode 100644
index 0000000..6ff7b7c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileConnectionRepository : MobileConnectionRepository {
+    private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
+    override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow
+
+    fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
+        _subscriptionsModelFlow.value = model
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
similarity index 66%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 0d15268..c88d468 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -18,11 +18,11 @@
 
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.mobile.MobileMappings.Config
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
+class FakeMobileConnectionsRepository : MobileConnectionsRepository {
     private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
     override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
 
@@ -30,22 +30,27 @@
         MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
     override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
 
-    private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>()
-    override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> {
-        return subIdFlows[subId]
-            ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it }
+    private val _defaultDataSubRatConfig = MutableStateFlow(Config())
+    override val defaultDataSubRatConfig = _defaultDataSubRatConfig
+
+    private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
     }
 
     fun setSubscriptions(subs: List<SubscriptionInfo>) {
         _subscriptionsFlow.value = subs
     }
 
+    fun setDefaultDataSubRatConfig(config: Config) {
+        _defaultDataSubRatConfig.value = config
+    }
+
     fun setActiveMobileDataSubscriptionId(subId: Int) {
         _activeMobileDataSubscriptionId.value = subId
     }
 
-    fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) {
-        val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet")
-        subscription.value = model
+    fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) {
+        subIdRepos[subId] = repo
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
new file mode 100644
index 0000000..775e6db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileConnectionRepositoryImpl
+
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+
+        underTest =
+            MobileConnectionRepositoryImpl(
+                SUB_1_ID,
+                telephonyManager,
+                IMMEDIATE,
+                logger,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testFlowForSubId_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(MobileSubscriptionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly_toggles() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<ServiceStateListener>()
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+            callback.onServiceStateChanged(serviceState)
+            serviceState.isEmergencyOnly = false
+            callback.onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_signalStrengths_levelsUpdate() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+            val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+            callback.onSignalStrengthsChanged(strength)
+
+            assertThat(latest?.isGsm).isEqualTo(true)
+            assertThat(latest?.primaryLevel).isEqualTo(1)
+            assertThat(latest?.cdmaLevel).isEqualTo(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(100, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(100)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataActivity() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
+            callback.onDataActivity(3)
+
+            assertThat(latest?.dataActivityDirection).isEqualTo(3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_carrierNetworkChange() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
+            callback.onCarrierNetworkChange(true)
+
+            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val type = NETWORK_TYPE_UNKNOWN
+            val expected = DefaultNetworkType(type)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_updatesUsingDefault() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+            val type = NETWORK_TYPE_LTE
+            val expected = DefaultNetworkType(type)
+            val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_updatesUsingOverride() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+            val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+            val expected = OverrideNetworkType(type)
+            val ti =
+                mock<TelephonyDisplayInfo>().also {
+                    whenever(it.overrideNetworkType).thenReturn(type)
+                }
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    /** Convenience constructor for SignalStrength */
+    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+        val signalStrength = mock<SignalStrength>()
+        whenever(signalStrength.isGsm).thenReturn(isGsm)
+        whenever(signalStrength.level).thenReturn(gsmLevel)
+        val cdmaStrength =
+            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+            .thenReturn(listOf(cdmaStrength))
+
+        return signalStrength
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
new file mode 100644
index 0000000..326e0d281
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionsRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileConnectionsRepositoryImpl
+
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(
+                broadcastDispatcher.broadcastFlow(
+                    any(),
+                    nullable(),
+                    ArgumentMatchers.anyInt(),
+                    nullable(),
+                )
+            )
+            .thenReturn(flowOf(Unit))
+
+        underTest =
+            MobileConnectionsRepositoryImpl(
+                subscriptionManager,
+                telephonyManager,
+                logger,
+                broadcastDispatcher,
+                context,
+                IMMEDIATE,
+                scope,
+                mock(),
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testSubscriptions_initiallyEmpty() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+        }
+
+    @Test
+    fun testSubscriptions_listUpdates() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_removingSub_updatesList() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            // WHEN 2 networks show up
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // WHEN one network is removed
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // THEN the subscriptions list represents the newest change
+            assertThat(latest).isEqualTo(listOf(SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.activeMobileDataSubscriptionId.value)
+                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_updates() =
+        runBlocking(IMMEDIATE) {
+            var active: Int? = null
+
+            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(active).isEqualTo(SUB_2_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_validSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_1_ID)
+
+            assertThat(repo1).isSameInstanceAs(repo2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionCache_clearsInvalidSubscriptions() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // Get repos to trigger caching
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+
+            assertThat(underTest.getSubIdRepoCache())
+                .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
+
+            // SUB_2 disappears
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_invalidSubId_throws() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            assertThrows(IllegalArgumentException::class.java) {
+                underTest.getRepoForSubId(SUB_1_ID)
+            }
+
+            job.cancel()
+        }
+
+    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+        verify(subscriptionManager)
+            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
deleted file mode 100644
index 316b795..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
+++ /dev/null
@@ -1,360 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-class MobileSubscriptionRepositoryTest : SysuiTestCase() {
-    private lateinit var underTest: MobileSubscriptionRepositoryImpl
-
-    @Mock private lateinit var subscriptionManager: SubscriptionManager
-    @Mock private lateinit var telephonyManager: TelephonyManager
-    private val scope = CoroutineScope(IMMEDIATE)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
-            MobileSubscriptionRepositoryImpl(
-                subscriptionManager,
-                telephonyManager,
-                IMMEDIATE,
-                scope,
-            )
-    }
-
-    @After
-    fun tearDown() {
-        scope.cancel()
-    }
-
-    @Test
-    fun testSubscriptions_initiallyEmpty() =
-        runBlocking(IMMEDIATE) {
-            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
-        }
-
-    @Test
-    fun testSubscriptions_listUpdates() =
-        runBlocking(IMMEDIATE) {
-            var latest: List<SubscriptionInfo>? = null
-
-            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_1, SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
-            job.cancel()
-        }
-
-    @Test
-    fun testSubscriptions_removingSub_updatesList() =
-        runBlocking(IMMEDIATE) {
-            var latest: List<SubscriptionInfo>? = null
-
-            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
-            // WHEN 2 networks show up
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_1, SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            // WHEN one network is removed
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            // THEN the subscriptions list represents the newest change
-            assertThat(latest).isEqualTo(listOf(SUB_2))
-
-            job.cancel()
-        }
-
-    @Test
-    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
-        runBlocking(IMMEDIATE) {
-            assertThat(underTest.activeMobileDataSubscriptionId.value)
-                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
-        }
-
-    @Test
-    fun testActiveDataSubscriptionId_updates() =
-        runBlocking(IMMEDIATE) {
-            var active: Int? = null
-
-            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
-
-            getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
-            assertThat(active).isEqualTo(SUB_2_ID)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_default() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            assertThat(latest).isEqualTo(MobileSubscriptionModel())
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_emergencyOnly() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val serviceState = ServiceState()
-            serviceState.isEmergencyOnly = true
-
-            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
-
-            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_emergencyOnly_toggles() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<ServiceStateListener>()
-            val serviceState = ServiceState()
-            serviceState.isEmergencyOnly = true
-            callback.onServiceStateChanged(serviceState)
-            serviceState.isEmergencyOnly = false
-            callback.onServiceStateChanged(serviceState)
-
-            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_signalStrengths_levelsUpdate() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<SignalStrengthsListener>()
-            val strength = signalStrength(1, 2, true)
-            callback.onSignalStrengthsChanged(strength)
-
-            assertThat(latest?.isGsm).isEqualTo(true)
-            assertThat(latest?.primaryLevel).isEqualTo(1)
-            assertThat(latest?.cdmaLevel).isEqualTo(2)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_dataConnectionState() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DataConnectionStateListener>()
-            callback.onDataConnectionStateChanged(100, 200 /* unused */)
-
-            assertThat(latest?.dataConnectionState).isEqualTo(100)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_dataActivity() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DataActivityListener>()
-            callback.onDataActivity(3)
-
-            assertThat(latest?.dataActivityDirection).isEqualTo(3)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_carrierNetworkChange() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
-            callback.onCarrierNetworkChange(true)
-
-            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_displayInfo() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DisplayInfoListener>()
-            val ti = mock<TelephonyDisplayInfo>()
-            callback.onDisplayInfoChanged(ti)
-
-            assertThat(latest?.displayInfo).isEqualTo(ti)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_isCached() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            val state1 = underTest.getFlowForSubId(SUB_1_ID)
-            val state2 = underTest.getFlowForSubId(SUB_1_ID)
-
-            assertThat(state1).isEqualTo(state2)
-        }
-
-    @Test
-    fun testFlowForSubId_isRemovedAfterFinish() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-
-            // Start collecting on some flow
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            // There should be once cached flow now
-            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
-
-            // When the job is canceled, the cache should be cleared
-            job.cancel()
-
-            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0)
-        }
-
-    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
-        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
-        verify(subscriptionManager)
-            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
-        return callbackCaptor.value!!
-    }
-
-    private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener =
-        getTelephonyCallbackForType()
-
-    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
-        val callbackCaptor = argumentCaptor<TelephonyCallback>()
-        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
-        return callbackCaptor.allValues
-    }
-
-    private inline fun <reified T> getTelephonyCallbackForType(): T {
-        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
-        assertThat(cbs.size).isEqualTo(1)
-        return cbs[0]
-    }
-
-    /** Convenience constructor for SignalStrength */
-    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
-        val signalStrength = mock<SignalStrength>()
-        whenever(signalStrength.isGsm).thenReturn(isGsm)
-        whenever(signalStrength.level).thenReturn(gsmLevel)
-        val cdmaStrength =
-            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
-        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
-            .thenReturn(listOf(cdmaStrength))
-
-        return signalStrength
-    }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-        private const val SUB_1_ID = 1
-        private val SUB_1 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
-        private const val SUB_2_ID = 2
-        private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 8ec68f3..cd4dbeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -23,7 +23,7 @@
 
 class FakeMobileIconInteractor : MobileIconInteractor {
     private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
-    override val iconGroup = _iconGroup
+    override val networkTypeIconGroup = _iconGroup
 
     private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
     override val isEmergencyOnly = _isEmergencyOnly
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
new file mode 100644
index 0000000..2bd2286
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) :
+    MobileIconsInteractor {
+    val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
+    val LTE_KEY = mobileMappings.toIconKey(LTE)
+    val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
+    val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE)
+
+    /**
+     * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to
+     * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for
+     * the exhaustive set of icons
+     */
+    val TEST_MAPPING: Map<String, MobileIconGroup> =
+        mapOf(
+            THREE_G_KEY to TelephonyIcons.THREE_G,
+            LTE_KEY to TelephonyIcons.LTE,
+            FOUR_G_KEY to TelephonyIcons.FOUR_G,
+            FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
+        )
+
+    private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val filteredSubscriptions = _filteredSubscriptions
+
+    private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
+    override val defaultMobileIconMapping = _defaultMobileIconMapping
+
+    private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON)
+    override val defaultMobileIconGroup = _defaultMobileIconGroup
+
+    private val _isUserSetup = MutableStateFlow(true)
+    override val isUserSetup = _isUserSetup
+
+    /** Always returns a new fake interactor */
+    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
+        return FakeMobileIconInteractor()
+    }
+
+    companion object {
+        val DEFAULT_ICON = TelephonyIcons.G
+
+        // Use [MobileMappings] to define some simple definitions
+        const val THREE_G = NETWORK_TYPE_GSM
+        const val LTE = NETWORK_TYPE_LTE
+        const val FOUR_G = NETWORK_TYPE_UMTS
+        const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 2f07d9c..ff44af4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -18,10 +18,19 @@
 
 import android.telephony.CellSignalStrength
 import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -29,26 +38,33 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
 
 @SmallTest
 class MobileIconInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconInteractor
-    private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository()
-    private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID)
+    private val mobileMappingsProxy = FakeMobileMappingsProxy()
+    private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
+    private val connectionRepository = FakeMobileConnectionRepository()
 
     @Before
     fun setUp() {
-        underTest = MobileIconInteractorImpl(sub1Flow)
+        underTest =
+            MobileIconInteractorImpl(
+                mobileIconsInteractor.defaultMobileIconMapping,
+                mobileIconsInteractor.defaultMobileIconGroup,
+                mobileMappingsProxy,
+                connectionRepository,
+            )
     }
 
     @Test
     fun gsm_level_default_unknown() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(isGsm = true),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -62,13 +78,12 @@
     @Test
     fun gsm_usesGsmLevel() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(
                     isGsm = true,
                     primaryLevel = GSM_LEVEL,
                     cdmaLevel = CDMA_LEVEL
                 ),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -82,9 +97,8 @@
     @Test
     fun cdma_level_default_unknown() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(isGsm = false),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -97,13 +111,12 @@
     @Test
     fun cdma_usesCdmaLevel() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(
                     isGsm = false,
                     primaryLevel = GSM_LEVEL,
                     cdmaLevel = CDMA_LEVEL
                 ),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -114,6 +127,75 @@
             job.cancel()
         }
 
+    @Test
+    fun iconGroup_three_g() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_updates_on_change() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    resolvedNetworkType = DefaultNetworkType(FOUR_G),
+                ),
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_5g_override_type() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.NR_5G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_default_if_no_lookup() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
+                ),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON)
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
@@ -123,9 +205,5 @@
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
-        private const val SUB_2_ID = 2
-        private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
     }
 }
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 89ad9cb..b01efd1 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
@@ -19,12 +19,14 @@
 import android.telephony.SubscriptionInfo
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -39,7 +41,9 @@
 class MobileIconsInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconsInteractor
     private val userSetupRepository = FakeUserSetupRepository()
-    private val subscriptionsRepository = FakeMobileSubscriptionRepository()
+    private val subscriptionsRepository = FakeMobileConnectionsRepository()
+    private val mobileMappingsProxy = FakeMobileMappingsProxy()
+    private val scope = CoroutineScope(IMMEDIATE)
 
     @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
 
@@ -47,10 +51,12 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         underTest =
-            MobileIconsInteractor(
+            MobileIconsInteractorImpl(
                 subscriptionsRepository,
                 carrierConfigTracker,
+                mobileMappingsProxy,
                 userSetupRepository,
+                scope
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
new file mode 100644
index 0000000..6d8d902
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.util
+
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.settingslib.mobile.TelephonyIcons
+
+class FakeMobileMappingsProxy : MobileMappingsProxy {
+    private var iconMap = mapOf<String, MobileIconGroup>()
+    private var defaultIcons = TelephonyIcons.THREE_G
+
+    fun setIconMap(map: Map<String, MobileIconGroup>) {
+        iconMap = map
+    }
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap
+    fun getIconMap() = iconMap
+
+    fun setDefaultIcons(group: MobileIconGroup) {
+        defaultIcons = group
+    }
+    override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons
+    fun getDefaultIcons(): MobileIconGroup = defaultIcons
+
+    override fun toIconKey(networkType: Int): String {
+        return networkType.toString()
+    }
+
+    override fun toIconKeyOverride(networkType: Int): String {
+        return toIconKey(networkType) + "_override"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index f751afc..2f18ce3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -27,6 +27,9 @@
     private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
 
+    private val _isWifiDefault: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
+
     private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
         MutableStateFlow(WifiNetworkModel.Inactive)
     override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
@@ -38,6 +41,10 @@
         _isWifiEnabled.value = enabled
     }
 
+    fun setIsWifiDefault(default: Boolean) {
+        _isWifiDefault.value = default
+    }
+
     fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) {
         _wifiNetwork.value = wifiNetworkModel
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index 0ba0bd6..a64a4bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -222,6 +222,83 @@
     }
 
     @Test
+    fun isWifiDefault_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_wifiNetwork_isTrue() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(
+            NETWORK,
+            createWifiNetworkCapabilities(wifiInfo)
+        )
+
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_cellularVcnNetwork_isTrue() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(VcnTransportInfo(PRIMARY_WIFI_INFO))
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_cellularNotVcnNetwork_isFalse() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        val capabilities = mock<NetworkCapabilities>().apply {
+            whenever(this.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+            whenever(this.transportInfo).thenReturn(mock())
+        }
+
+        getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
+
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isWifiDefault_wifiNetworkLost_isFalse() = runBlocking(IMMEDIATE) {
+        val job = underTest.isWifiDefault.launchIn(this)
+
+        // First, add a network
+        getDefaultNetworkCallback()
+            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO))
+        assertThat(underTest.isWifiDefault.value).isTrue()
+
+        // WHEN the network is lost
+        getDefaultNetworkCallback().onLost(NETWORK)
+
+        // THEN we update to false
+        assertThat(underTest.isWifiDefault.value).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
         val job = underTest
@@ -745,6 +822,12 @@
         return callbackCaptor.value!!
     }
 
+    private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+        val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+        verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
     private fun createWifiNetworkCapabilities(
         wifiInfo: WifiInfo,
         isValidated: Boolean = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
index 39b886a..71b8bab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -178,6 +178,29 @@
     }
 
     @Test
+    fun isDefault_matchesRepoIsDefault() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .isDefault
+            .onEach { latest = it }
+            .launchIn(this)
+
+        wifiRepository.setIsWifiDefault(true)
+        yield()
+        assertThat(latest).isTrue()
+
+        wifiRepository.setIsWifiDefault(false)
+        yield()
+        assertThat(latest).isFalse()
+
+        wifiRepository.setIsWifiDefault(true)
+        yield()
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) {
         val wifiNetwork = WifiNetworkModel.Active(
             networkId = 45,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 4efb135..c584109 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -30,6 +30,9 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
@@ -63,11 +66,13 @@
     private lateinit var connectivityConstants: ConnectivityConstants
     @Mock
     private lateinit var wifiConstants: WifiConstants
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
     private lateinit var viewModel: WifiViewModel
     private lateinit var scope: CoroutineScope
+    private lateinit var airplaneModeViewModel: AirplaneModeViewModel
 
     @JvmField @Rule
     val instantTaskExecutor = InstantTaskExecutorRule()
@@ -77,12 +82,22 @@
         MockitoAnnotations.initMocks(this)
         testableLooper = TestableLooper.get(this)
 
+        airplaneModeRepository = FakeAirplaneModeRepository()
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
         scope = CoroutineScope(Dispatchers.Unconfined)
+        airplaneModeViewModel = AirplaneModeViewModel(
+            AirplaneModeInteractor(
+                airplaneModeRepository,
+                connectivityRepository,
+            ),
+            logger,
+            scope,
+        )
         viewModel = WifiViewModel(
+            airplaneModeViewModel,
             connectivityConstants,
             context,
             logger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index a3ad028..a1afcd7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -22,11 +22,14 @@
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_CONNECTION_STRENGTH
 import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTION
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
@@ -64,19 +67,31 @@
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
+    private lateinit var airplaneModeViewModel: AirplaneModeViewModel
     private lateinit var scope: CoroutineScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
         scope = CoroutineScope(IMMEDIATE)
+        airplaneModeViewModel =
+            AirplaneModeViewModel(
+                AirplaneModeInteractor(
+                    airplaneModeRepository,
+                    connectivityRepository,
+                ),
+                logger,
+                scope,
+            )
     }
 
     @After
@@ -88,6 +103,7 @@
     fun wifiIcon() =
         runBlocking(IMMEDIATE) {
             wifiRepository.setIsWifiEnabled(testCase.enabled)
+            wifiRepository.setIsWifiDefault(testCase.isDefault)
             connectivityRepository.setForceHiddenIcons(
                 if (testCase.forceHidden) {
                     setOf(ConnectivitySlot.WIFI)
@@ -101,6 +117,7 @@
                 .thenReturn(testCase.hasDataCapabilities)
             underTest =
                 WifiViewModel(
+                    airplaneModeViewModel,
                     connectivityConstants,
                     context,
                     logger,
@@ -125,19 +142,12 @@
                 } else {
                     testCase.expected.contentDescription.invoke(context)
                 }
-            assertThat(iconFlow.value?.contentDescription?.getAsString())
+            assertThat(iconFlow.value?.contentDescription?.loadContentDescription(context))
                 .isEqualTo(expectedContentDescription)
 
             job.cancel()
         }
 
-    private fun ContentDescription.getAsString(): String? {
-        return when (this) {
-            is ContentDescription.Loaded -> this.description
-            is ContentDescription.Resource -> context.getString(this.res)
-        }
-    }
-
     internal data class Expected(
         /** The resource that should be used for the icon. */
         @DrawableRes val iconResource: Int,
@@ -159,6 +169,7 @@
         val forceHidden: Boolean = false,
         val alwaysShowIconWhenEnabled: Boolean = false,
         val hasDataCapabilities: Boolean = true,
+        val isDefault: Boolean = false,
         val network: WifiNetworkModel,
 
         /** The expected output. Null if we expect the output to be null. */
@@ -169,6 +180,7 @@
                 "forceHidden=$forceHidden, " +
                 "showWhenEnabled=$alwaysShowIconWhenEnabled, " +
                 "hasDataCaps=$hasDataCapabilities, " +
+                "isDefault=$isDefault, " +
                 "network=$network) then " +
                 "EXPECTED($expected)"
         }
@@ -303,6 +315,46 @@
                         ),
                 ),
 
+                // isDefault = true => all Inactive and Active networks shown
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Inactive,
+                    expected =
+                        Expected(
+                            iconResource = WIFI_NO_NETWORK,
+                            contentDescription = { context ->
+                                "${context.getString(WIFI_NO_CONNECTION)}," +
+                                    context.getString(NO_INTERNET)
+                            },
+                            description = "No network icon",
+                        ),
+                ),
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3),
+                    expected =
+                        Expected(
+                            iconResource = WIFI_NO_INTERNET_ICONS[3],
+                            contentDescription = { context ->
+                                "${context.getString(WIFI_CONNECTION_STRENGTH[3])}," +
+                                    context.getString(NO_INTERNET)
+                            },
+                            description = "No internet level 3 icon",
+                        ),
+                ),
+                TestCase(
+                    isDefault = true,
+                    network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 1),
+                    expected =
+                        Expected(
+                            iconResource = WIFI_FULL_ICONS[1],
+                            contentDescription = { context ->
+                                context.getString(WIFI_CONNECTION_STRENGTH[1])
+                            },
+                            description = "Full internet level 1 icon",
+                        ),
+                ),
+
                 // network = CarrierMerged => not shown
                 TestCase(
                     network = WifiNetworkModel.CarrierMerged,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 3169eef..7d2c560 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -20,8 +20,12 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
@@ -55,19 +59,31 @@
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var connectivityConstants: ConnectivityConstants
     @Mock private lateinit var wifiConstants: WifiConstants
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
+    private lateinit var airplaneModeViewModel: AirplaneModeViewModel
     private lateinit var scope: CoroutineScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeRepository = FakeAirplaneModeRepository()
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
         wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
         scope = CoroutineScope(IMMEDIATE)
+        airplaneModeViewModel = AirplaneModeViewModel(
+            AirplaneModeInteractor(
+                airplaneModeRepository,
+                connectivityRepository,
+            ),
+            logger,
+            scope,
+        )
+
         createAndSetViewModel()
     }
 
@@ -76,6 +92,8 @@
         scope.cancel()
     }
 
+    // See [WifiViewModelIconParameterizedTest] for additional view model tests.
+
     // Note on testing: [WifiViewModel] exposes 3 different instances of
     // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact
     // same data for icon, activity, etc. flows. So, most of these tests will test just one of the
@@ -460,11 +478,64 @@
         job.cancel()
     }
 
+    @Test
+    fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .qs
+            .isAirplaneSpacerVisible
+            .onEach { latest = it }
+            .launchIn(this)
+
+        airplaneModeRepository.setIsAirplaneMode(false)
+        yield()
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .qs
+            .isAirplaneSpacerVisible
+            .onEach { latest = it }
+            .launchIn(this)
+
+        airplaneModeRepository.setIsAirplaneMode(true)
+        connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE))
+        yield()
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) {
+        var latest: Boolean? = null
+        val job = underTest
+            .qs
+            .isAirplaneSpacerVisible
+            .onEach { latest = it }
+            .launchIn(this)
+
+        airplaneModeRepository.setIsAirplaneMode(true)
+        yield()
+
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
     private fun createAndSetViewModel() {
         // [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow
         // creations rely on certain config values that we mock out in individual tests. This method
         // allows tests to create the view model only after those configs are correctly set up.
         underTest = WifiViewModel(
+            airplaneModeViewModel,
             connectivityConstants,
             context,
             logger,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 6ace404..915e999 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -23,8 +23,12 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.app.PendingIntent;
@@ -43,10 +47,14 @@
 import android.testing.TestableLooper;
 import android.view.ContentInfo;
 import android.view.View;
+import android.view.ViewRootImpl;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
 import android.widget.ImageButton;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+import android.window.WindowOnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
@@ -67,6 +75,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -229,6 +238,67 @@
     }
 
     @Test
+    public void testPredictiveBack_registerAndUnregister() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+
+        ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+        WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+                WindowOnBackInvokedDispatcher.class);
+        ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+                OnBackInvokedCallback.class);
+        when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+        view.setViewRootImpl(viewRoot);
+
+        /* verify that predictive back callback registered when RemoteInputView becomes visible */
+        view.onVisibilityAggregated(true);
+        verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                onBackInvokedCallbackCaptor.capture());
+
+        /* verify that same callback unregistered when RemoteInputView becomes invisible */
+        view.onVisibilityAggregated(false);
+        verify(backInvokedDispatcher).unregisterOnBackInvokedCallback(
+                eq(onBackInvokedCallbackCaptor.getValue()));
+    }
+
+    @Test
+    public void testUiPredictiveBack_openAndDispatchCallback() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+        ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+        WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+                WindowOnBackInvokedDispatcher.class);
+        ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+                OnBackInvokedCallback.class);
+        when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+        view.setViewRootImpl(viewRoot);
+        view.onVisibilityAggregated(true);
+        view.setEditTextReferenceToSelf();
+
+        /* capture the callback during registration */
+        verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                onBackInvokedCallbackCaptor.capture());
+
+        view.focus();
+
+        /* invoke the captured callback */
+        onBackInvokedCallbackCaptor.getValue().onBackInvoked();
+
+        /* verify that the RemoteInputView goes away */
+        assertEquals(view.getVisibility(), View.GONE);
+    }
+
+    @Test
     public void testUiEventLogging_openAndSend() throws Exception {
         NotificationTestHelper helper = new NotificationTestHelper(
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 6225d0c..9fbf159 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.temporarydisplay.chipbar
 
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.media.MediaRoute2Info
 import android.os.PowerManager
+import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -31,19 +28,19 @@
 import android.widget.TextView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
-import com.android.systemui.media.taptotransfer.sender.ChipStateSender
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEvents
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
 import com.google.common.truth.Truth.assertThat
@@ -53,7 +50,6 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
@@ -64,437 +60,293 @@
 class ChipbarCoordinatorTest : SysuiTestCase() {
     private lateinit var underTest: FakeChipbarCoordinator
 
-    @Mock
-    private lateinit var packageManager: PackageManager
-    @Mock
-    private lateinit var applicationInfo: ApplicationInfo
-    @Mock
-    private lateinit var logger: MediaTttLogger
-    @Mock
-    private lateinit var accessibilityManager: AccessibilityManager
-    @Mock
-    private lateinit var configurationController: ConfigurationController
-    @Mock
-    private lateinit var powerManager: PowerManager
-    @Mock
-    private lateinit var windowManager: WindowManager
-    @Mock
-    private lateinit var falsingManager: FalsingManager
-    @Mock
-    private lateinit var falsingCollector: FalsingCollector
-    @Mock
-    private lateinit var viewUtil: ViewUtil
-    private lateinit var fakeAppIconDrawable: Drawable
+    @Mock private lateinit var logger: MediaTttLogger
+    @Mock private lateinit var accessibilityManager: AccessibilityManager
+    @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var powerManager: PowerManager
+    @Mock private lateinit var windowManager: WindowManager
+    @Mock private lateinit var falsingManager: FalsingManager
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var viewUtil: ViewUtil
+    @Mock private lateinit var vibratorHelper: VibratorHelper
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
-    private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
-        fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
-        whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME)
-        whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
-        whenever(packageManager.getApplicationInfo(
-            eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>()
-        )).thenReturn(applicationInfo)
-        context.setMockPackageManager(packageManager)
+        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
 
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
         uiEventLoggerFake = UiEventLoggerFake()
-        senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
-        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
-
-        underTest = FakeChipbarCoordinator(
-            context,
-            logger,
-            windowManager,
-            fakeExecutor,
-            accessibilityManager,
-            configurationController,
-            powerManager,
-            senderUiEventLogger,
-            falsingManager,
-            falsingCollector,
-            viewUtil,
-        )
+        underTest =
+            FakeChipbarCoordinator(
+                context,
+                logger,
+                windowManager,
+                fakeExecutor,
+                accessibilityManager,
+                configurationController,
+                powerManager,
+                falsingManager,
+                falsingCollector,
+                viewUtil,
+                vibratorHelper,
+            )
         underTest.start()
     }
 
     @Test
-    fun almostCloseToStartCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
-        val state = almostCloseToStartCast()
-        underTest.displayView(state)
+    fun displayView_loadedIcon_correctlyRendered() {
+        val drawable = context.getDrawable(R.drawable.ic_celebration)!!
 
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun almostCloseToEndCast_appIcon_deviceName_noLoadingIcon_noUndo_noFailureIcon() {
-        val state = almostCloseToEndCast()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
-        val state = transferToReceiverTriggered()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceTriggered_appIcon_loadingIcon_noUndo_noFailureIcon() {
-        val state = transferToThisDeviceTriggered()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
-        val state = transferToReceiverSucceeded()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_nullUndoRunnable_noUndo() {
-        underTest.displayView(transferToReceiverSucceeded(undoCallback = null))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_undoWithClick() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
-        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isFalse()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
-        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToReceiverSucceeded(undoCallback))
-
-        getChipView().getUndoButton().performClick()
-
-        assertThat(getChipView().getChipText()).isEqualTo(
-            transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
-            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id
-        )
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_appIcon_deviceName_noLoadingIcon_noFailureIcon() {
-        val state = transferToThisDeviceSucceeded()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(chipView.getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_nullUndoRunnable_noUndo() {
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback = null))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_withUndoRunnable_undoWithClick() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-
-        val chipView = getChipView()
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.VISIBLE)
-        assertThat(chipView.getUndoButton().hasOnClickListeners()).isTrue()
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_withUndoRunnable_undoButtonClickRunsRunnable() {
-        var undoCallbackCalled = false
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {
-                undoCallbackCalled = true
-            }
-        }
-
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-        getChipView().getUndoButton().performClick()
-
-        assertThat(undoCallbackCalled).isTrue()
-    }
-
-    @Test
-    fun transferToThisDeviceSucceeded_undoButtonClick_switchesToTransferToReceiverTriggered() {
-        val undoCallback = object : IUndoMediaTransferCallback.Stub() {
-            override fun onUndoTriggered() {}
-        }
-        underTest.displayView(transferToThisDeviceSucceeded(undoCallback))
-
-        getChipView().getUndoButton().performClick()
-
-        assertThat(getChipView().getChipText()).isEqualTo(
-            transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(
-            MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id
-        )
-    }
-
-    @Test
-    fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
-        val state = transferToReceiverFailed()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(getChipView().getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() {
-        val state = transferToThisDeviceFailed()
-        underTest.displayView(state)
-
-        val chipView = getChipView()
-        assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable)
-        assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME)
-        assertThat(getChipView().getChipText()).isEqualTo(
-            state.state.getChipTextString(context, OTHER_DEVICE_NAME)
-        )
-        assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE)
-        assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE)
-        assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun changeFromAlmostCloseToStartToTransferTriggered_loadingIconAppears() {
-        underTest.displayView(almostCloseToStartCast())
-        underTest.displayView(transferToReceiverTriggered())
-
-        assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.VISIBLE)
-    }
-
-    @Test
-    fun changeFromTransferTriggeredToTransferSucceeded_loadingIconDisappears() {
-        underTest.displayView(transferToReceiverTriggered())
-        underTest.displayView(transferToReceiverSucceeded())
-
-        assertThat(getChipView().getLoadingIconVisibility()).isEqualTo(View.GONE)
-    }
-
-    @Test
-    fun changeFromTransferTriggeredToTransferSucceeded_undoButtonAppears() {
-        underTest.displayView(transferToReceiverTriggered())
         underTest.displayView(
-            transferToReceiverSucceeded(
-                object : IUndoMediaTransferCallback.Stub() {
-                    override fun onUndoTriggered() {}
-                }
+            ChipbarInfo(
+                Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+                Text.Loaded("text"),
+                endItem = null,
             )
         )
 
-        assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.VISIBLE)
+        val iconView = getChipbarView().getStartIconView()
+        assertThat(iconView.drawable).isEqualTo(drawable)
+        assertThat(iconView.contentDescription).isEqualTo("loadedCD")
     }
 
     @Test
-    fun changeFromTransferSucceededToAlmostCloseToStart_undoButtonDisappears() {
-        underTest.displayView(transferToReceiverSucceeded())
-        underTest.displayView(almostCloseToStartCast())
+    fun displayView_resourceIcon_correctlyRendered() {
+        val contentDescription = ContentDescription.Resource(R.string.controls_error_timeout)
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.drawable.ic_cake, contentDescription),
+                Text.Loaded("text"),
+                endItem = null,
+            )
+        )
 
-        assertThat(getChipView().getUndoButton().visibility).isEqualTo(View.GONE)
+        val iconView = getChipbarView().getStartIconView()
+        assertThat(iconView.contentDescription)
+            .isEqualTo(contentDescription.loadContentDescription(context))
     }
 
     @Test
-    fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() {
-        underTest.displayView(transferToReceiverTriggered())
-        underTest.displayView(transferToReceiverFailed())
+    fun displayView_loadedText_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("display view text here"),
+                endItem = null,
+            )
+        )
 
-        assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(getChipbarView().getChipText()).isEqualTo("display view text here")
     }
 
-    private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
+    @Test
+    fun displayView_resourceText_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Resource(R.string.screenrecord_start_error),
+                endItem = null,
+            )
+        )
+
+        assertThat(getChipbarView().getChipText())
+            .isEqualTo(context.getString(R.string.screenrecord_start_error))
+    }
+
+    @Test
+    fun displayView_endItemNull_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = null,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemLoading_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemError_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = ChipbarEndItem.Error,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun displayView_endItemButton_correctlyRendered() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        onClickListener = {},
+                    ),
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().text).isEqualTo("button text")
+        assertThat(chipbarView.getEndButton().hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun displayView_endItemButtonClicked_falseTap_listenerNotRun() {
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true)
+        var isClicked = false
+        val buttonClickListener = View.OnClickListener { isClicked = true }
+
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        buttonClickListener,
+                    ),
+            )
+        )
+
+        getChipbarView().getEndButton().performClick()
+
+        assertThat(isClicked).isFalse()
+    }
+
+    @Test
+    fun displayView_endItemButtonClicked_notFalseTap_listenerRun() {
+        whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false)
+        var isClicked = false
+        val buttonClickListener = View.OnClickListener { isClicked = true }
+
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem =
+                    ChipbarEndItem.Button(
+                        Text.Loaded("button text"),
+                        buttonClickListener,
+                    ),
+            )
+        )
+
+        getChipbarView().getEndButton().performClick()
+
+        assertThat(isClicked).isTrue()
+    }
+
+    @Test
+    fun displayView_vibrationEffect_doubleClickEffect() {
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Resource(R.id.check_box, null),
+                Text.Loaded("text"),
+                endItem = null,
+                vibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK),
+            )
+        )
+
+        verify(vibratorHelper).vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))
+    }
+
+    @Test
+    fun updateView_viewUpdated() {
+        // First, display a view
+        val drawable = context.getDrawable(R.drawable.ic_celebration)!!
+
+        underTest.displayView(
+            ChipbarInfo(
+                Icon.Loaded(drawable, contentDescription = ContentDescription.Loaded("loadedCD")),
+                Text.Loaded("title text"),
+                endItem = ChipbarEndItem.Loading,
+            )
+        )
+
+        val chipbarView = getChipbarView()
+        assertThat(chipbarView.getStartIconView().drawable).isEqualTo(drawable)
+        assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("loadedCD")
+        assertThat(chipbarView.getChipText()).isEqualTo("title text")
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+
+        // WHEN the view is updated
+        val newDrawable = context.getDrawable(R.drawable.ic_cake)!!
+        underTest.updateView(
+            ChipbarInfo(
+                Icon.Loaded(newDrawable, ContentDescription.Loaded("new CD")),
+                Text.Loaded("new title text"),
+                endItem = ChipbarEndItem.Error,
+            ),
+            chipbarView
+        )
+
+        // THEN we display the new view
+        assertThat(chipbarView.getStartIconView().drawable).isEqualTo(newDrawable)
+        assertThat(chipbarView.getStartIconView().contentDescription).isEqualTo("new CD")
+        assertThat(chipbarView.getChipText()).isEqualTo("new title text")
+        assertThat(chipbarView.getLoadingIcon().visibility).isEqualTo(View.GONE)
+        assertThat(chipbarView.getErrorIcon().visibility).isEqualTo(View.VISIBLE)
+        assertThat(chipbarView.getEndButton().visibility).isEqualTo(View.GONE)
+    }
+
+    private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon)
 
     private fun ViewGroup.getChipText(): String =
         (this.requireViewById<TextView>(R.id.text)).text as String
 
-    private fun ViewGroup.getLoadingIconVisibility(): Int =
-        this.requireViewById<View>(R.id.loading).visibility
+    private fun ViewGroup.getLoadingIcon(): View = this.requireViewById(R.id.loading)
 
-    private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo)
+    private fun ViewGroup.getEndButton(): TextView = this.requireViewById(R.id.end_button)
 
-    private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon)
+    private fun ViewGroup.getErrorIcon(): View = this.requireViewById(R.id.error)
 
-    private fun getChipView(): ViewGroup {
+    private fun getChipbarView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
         verify(windowManager).addView(viewCaptor.capture(), any())
         return viewCaptor.value as ViewGroup
     }
-
-    // TODO(b/245610654): For now, the below methods are duplicated between this test and
-    //   [MediaTttSenderCoordinatorTest]. Once we define a generic API for [ChipbarCoordinator],
-    //   these will no longer be duplicated.
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToStartCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun almostCloseToEndCast() =
-        ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceTriggered() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToReceiverFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
-
-    /** Helper method providing default parameters to not clutter up the tests. */
-    private fun transferToThisDeviceFailed() =
-        ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo)
 }
 
-private const val APP_NAME = "Fake app name"
-private const val OTHER_DEVICE_NAME = "My Tablet"
-private const val PACKAGE_NAME = "com.android.systemui"
 private const val TIMEOUT = 10000
-
-private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
-    .addFeature("feature")
-    .setClientPackageName(PACKAGE_NAME)
-    .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index 10704ac..17d4023 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -24,8 +24,8 @@
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger
-import com.android.systemui.media.taptotransfer.sender.MediaTttSenderUiEventLogger
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
@@ -39,10 +39,10 @@
     accessibilityManager: AccessibilityManager,
     configurationController: ConfigurationController,
     powerManager: PowerManager,
-    uiEventLogger: MediaTttSenderUiEventLogger,
     falsingManager: FalsingManager,
     falsingCollector: FalsingCollector,
     viewUtil: ViewUtil,
+    vibratorHelper: VibratorHelper,
 ) :
     ChipbarCoordinator(
         context,
@@ -52,10 +52,10 @@
         accessibilityManager,
         configurationController,
         powerManager,
-        uiEventLogger,
         falsingManager,
         falsingCollector,
         viewUtil,
+        vibratorHelper,
     ) {
     override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
index d951f36..525d837 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
@@ -110,7 +110,7 @@
         val thirdExpectedValue =
             setUpUsers(
                 count = 2,
-                hasGuest = true,
+                isLastGuestUser = true,
                 selectedIndex = 1,
             )
         underTest.refreshUsers()
@@ -121,21 +121,25 @@
     }
 
     @Test
-    fun `refreshUsers - sorts by creation time`() = runSelfCancelingTest {
+    fun `refreshUsers - sorts by creation time - guest user last`() = runSelfCancelingTest {
         underTest = create(this)
         val unsortedUsers =
             setUpUsers(
                 count = 3,
                 selectedIndex = 0,
+                isLastGuestUser = true,
             )
-        unsortedUsers[0].creationTime = 900
-        unsortedUsers[1].creationTime = 700
-        unsortedUsers[2].creationTime = 999
-        val expectedUsers = listOf(unsortedUsers[1], unsortedUsers[0], unsortedUsers[2])
+        unsortedUsers[0].creationTime = 999
+        unsortedUsers[1].creationTime = 900
+        unsortedUsers[2].creationTime = 950
+        val expectedUsers =
+            listOf(
+                unsortedUsers[1],
+                unsortedUsers[0],
+                unsortedUsers[2], // last because this is the guest
+            )
         var userInfos: List<UserInfo>? = null
-        var selectedUserInfo: UserInfo? = null
         underTest.userInfos.onEach { userInfos = it }.launchIn(this)
-        underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
 
         underTest.refreshUsers()
         assertThat(userInfos).isEqualTo(expectedUsers)
@@ -143,14 +147,14 @@
 
     private fun setUpUsers(
         count: Int,
-        hasGuest: Boolean = false,
+        isLastGuestUser: Boolean = false,
         selectedIndex: Int = 0,
     ): List<UserInfo> {
         val userInfos =
             (0 until count).map { index ->
                 createUserInfo(
                     index,
-                    isGuest = hasGuest && index == count - 1,
+                    isGuest = isLastGuestUser && index == count - 1,
                 )
             }
         whenever(manager.aliveUsers).thenReturn(userInfos)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
index d4b41c1..a363a03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -97,6 +97,7 @@
                         createUserRecord(2),
                         createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
                         createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                        createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
                     )
                 )
             var models: List<UserModel>? = null
@@ -176,15 +177,17 @@
                         createUserRecord(2),
                         createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
                         createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+                        createActionRecord(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT),
                     )
                 )
             var models: List<UserActionModel>? = null
             val job = underTest.actions.onEach { models = it }.launchIn(this)
 
-            assertThat(models).hasSize(3)
+            assertThat(models).hasSize(4)
             assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
             assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
             assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+            assertThat(models?.get(3)).isEqualTo(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
             job.cancel()
         }
 
@@ -200,6 +203,7 @@
             isAddUser = action == UserActionModel.ADD_USER,
             isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
             isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+            isManageUsers = action == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
index e80d516..f682e31 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -28,6 +28,7 @@
 import com.android.internal.R.drawable.ic_account_circle
 import com.android.systemui.R
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.user.data.model.UserSwitcherSettingsModel
 import com.android.systemui.user.data.source.UserRecord
 import com.android.systemui.user.domain.model.ShowDialogRequestModel
@@ -317,14 +318,16 @@
             keyguardRepository.setKeyguardShowing(false)
             var dialogRequest: ShowDialogRequestModel? = null
             val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+            val dialogShower: UserSwitchDialogController.DialogShower = mock()
 
-            underTest.executeAction(UserActionModel.ADD_USER)
+            underTest.executeAction(UserActionModel.ADD_USER, dialogShower)
             assertThat(dialogRequest)
                 .isEqualTo(
                     ShowDialogRequestModel.ShowAddUserDialog(
                         userHandle = userInfos[0].userHandle,
                         isKeyguardShowing = false,
                         showEphemeralMessage = false,
+                        dialogShower = dialogShower,
                     )
                 )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
index c3a9705..6a17c8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -64,13 +64,7 @@
     @Test
     fun `actions - not actionable when locked and not locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(false)
             keyguardRepository.setKeyguardShowing(false)
 
@@ -92,13 +86,7 @@
     @Test
     fun `actions - actionable when locked and not locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(true)
             keyguardRepository.setKeyguardShowing(false)
 
@@ -120,13 +108,7 @@
     @Test
     fun `actions - actionable when locked and locked`() =
         runBlocking(IMMEDIATE) {
-            userRepository.setActions(
-                listOf(
-                    UserActionModel.ENTER_GUEST_MODE,
-                    UserActionModel.ADD_USER,
-                    UserActionModel.ADD_SUPERVISED_USER,
-                )
-            )
+            setActions()
             userRepository.setActionableWhenLocked(true)
             keyguardRepository.setKeyguardShowing(true)
 
@@ -182,6 +164,10 @@
         verify(activityStarter).startActivity(any(), anyBoolean())
     }
 
+    private fun setActions() {
+        userRepository.setActions(UserActionModel.values().toList())
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index 0344e3f..c12a868 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -268,6 +268,26 @@
         }
 
     @Test
+    fun `menu actions`() =
+        runBlocking(IMMEDIATE) {
+            userRepository.setActions(UserActionModel.values().toList())
+            var actions: List<UserActionViewModel>? = null
+            val job = underTest.menu.onEach { actions = it }.launchIn(this)
+
+            assertThat(actions?.map { it.viewKey })
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE.ordinal.toLong(),
+                        UserActionModel.ADD_USER.ordinal.toLong(),
+                        UserActionModel.ADD_SUPERVISED_USER.ordinal.toLong(),
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT.ordinal.toLong(),
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
     fun `isFinishRequested - finishes when user is switched`() =
         runBlocking(IMMEDIATE) {
             setUsers(count = 2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 09da52e..fa7ebf6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -80,6 +80,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.colorextraction.ColorExtractor;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
@@ -91,6 +92,7 @@
 import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeExpansionStateManager;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.RankingBuilder;
@@ -190,6 +192,8 @@
     private NotificationShadeWindowView mNotificationShadeWindowView;
     @Mock
     private AuthController mAuthController;
+    @Mock
+    private ShadeExpansionStateManager mShadeExpansionStateManager;
 
     private SysUiState mSysUiState;
     private boolean mSysUiStateBubblesExpanded;
@@ -290,7 +294,7 @@
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager, mKeyguardStateController,
-                mScreenOffAnimationController, mAuthController);
+                mScreenOffAnimationController, mAuthController, mShadeExpansionStateManager);
         mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView);
         mNotificationShadeWindowController.attach();
 
@@ -343,7 +347,8 @@
                         mock(NotificationInterruptLogger.class),
                         mock(Handler.class),
                         mock(NotifPipelineFlags.class),
-                        mock(KeyguardNotificationVisibilityProvider.class)
+                        mock(KeyguardNotificationVisibilityProvider.class),
+                        mock(UiEventLogger.class)
                 );
         when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index 9635faf..e5316bc8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -22,6 +22,7 @@
 import android.os.PowerManager;
 import android.service.dreams.IDreamManager;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
@@ -46,7 +47,8 @@
             NotificationInterruptLogger logger,
             Handler mainHandler,
             NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider) {
+            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
+            UiEventLogger uiEventLogger) {
         super(contentResolver,
                 powerManager,
                 dreamManager,
@@ -58,7 +60,8 @@
                 logger,
                 mainHandler,
                 flags,
-                keyguardNotificationVisibilityProvider);
+                keyguardNotificationVisibilityProvider,
+                uiEventLogger);
         mUseHeadsUp = true;
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index cebe946..7af66f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -34,6 +34,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedEventCallback;
@@ -49,6 +51,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 /**
  * Tests for {@link WMShell}.
@@ -76,12 +79,14 @@
     @Mock UserTracker mUserTracker;
     @Mock ShellExecutor mSysUiMainExecutor;
     @Mock FloatingTasks mFloatingTasks;
+    @Mock DesktopMode mDesktopMode;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip),
                 Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks),
+                Optional.of(mDesktopMode),
                 mCommandQueue, mConfigurationController, mKeyguardStateController,
                 mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
                 mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
@@ -103,4 +108,12 @@
         verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class));
         verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class));
     }
+
+    @Test
+    public void initDesktopMode_registersListener() {
+        mWMShell.initDesktopMode(mDesktopMode);
+        verify(mDesktopMode).addListener(
+                any(DesktopModeTaskRepository.VisibleTasksListener.class),
+                any(Executor.class));
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index 5d52be2..a60b773 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -26,7 +26,7 @@
     private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>()
 
     init {
-        Flags.getFlagFields().forEach { field ->
+        Flags.flagFields.forEach { field ->
             val flag: Flag<*> = field.get(null) as Flag<*>
             knownFlagNames[flag.id] = field.name
         }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 725b1f4..0c12680 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.common.shared.model.Position
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -44,6 +45,9 @@
     private val _dozeAmount = MutableStateFlow(0f)
     override val dozeAmount: Flow<Float> = _dozeAmount
 
+    private val _statusBarState = MutableStateFlow(StatusBarState.SHADE)
+    override val statusBarState: Flow<StatusBarState> = _statusBarState
+
     override fun isKeyguardShowing(): Boolean {
         return _isKeyguardShowing.value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
index 5272585..c33ce5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.qs
 
-import android.view.View
+import com.android.systemui.animation.Expandable
 import com.android.systemui.qs.FgsManagerController.OnDialogDismissedListener
 import com.android.systemui.qs.FgsManagerController.OnNumberOfPackagesChangedListener
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -54,7 +54,7 @@
 
     override fun init() {}
 
-    override fun showDialog(viewLaunchedFrom: View?) {}
+    override fun showDialog(expandable: Expandable?) {}
 
     override fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) {
         numRunningPackagesListeners.add(listener)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
index 2a9aedd..325da4e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/footer/FooterActionsTestUtils.kt
@@ -57,7 +57,6 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.GlobalSettings
-import com.android.systemui.util.time.FakeSystemClock
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.test.TestCoroutineDispatcher
 
@@ -68,7 +67,6 @@
 class FooterActionsTestUtils(
     private val context: Context,
     private val testableLooper: TestableLooper,
-    private val fakeClock: FakeSystemClock = FakeSystemClock(),
 ) {
     /** Enable or disable the user switcher in the settings. */
     fun setUserSwitcherEnabled(settings: GlobalSettings, enabled: Boolean, userId: Int) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index 8d171be..69575a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -26,7 +26,9 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatcher
 import org.mockito.Mockito
+import org.mockito.Mockito.`when`
 import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
 
 /**
  * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
@@ -89,7 +91,8 @@
  *
  * @see Mockito.when
  */
-fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
 
 /**
  * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
diff --git a/proto/src/camera.proto b/proto/src/camera.proto
index 38d74e4..205e806 100644
--- a/proto/src/camera.proto
+++ b/proto/src/camera.proto
@@ -67,4 +67,6 @@
     optional int64 dynamic_range_profile = 14;
     // The stream use case
     optional int64 stream_use_case = 15;
+    // The color space of the stream
+    optional int32 color_space = 16;
 }
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index a94bfe2..12e7226 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -61,7 +61,7 @@
 
     // Notify the user that they should select an input method
     // Package: android
-    NOTE_SELECT_INPUT_METHOD = 8;
+    NOTE_SELECT_INPUT_METHOD = 8 [deprecated = true];
 
     // Notify the user about limited functionality before decryption
     // Package: android
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index bfa1b20..085a589 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -4144,7 +4144,7 @@
                     if (readEnabledAccessibilityServicesLocked(userState)) {
                         mSecurityPolicy.onEnabledServicesChangedLocked(userState.mUserId,
                                 userState.mEnabledServices);
-                        userState.updateCrashedServicesIfNeededLocked();
+                        userState.removeDisabledServicesFromTemporaryStatesLocked();
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mTouchExplorationGrantedAccessibilityServicesUri.equals(uri)) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 55dc196..0cb7209 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -381,18 +381,22 @@
     }
 
     /**
-     * Remove service from crashed service list if users disable it.
+     * Remove the service from the crashed and binding service lists if the user disabled it.
      */
-    void updateCrashedServicesIfNeededLocked() {
+    void removeDisabledServicesFromTemporaryStatesLocked() {
         for (int i = 0, count = mInstalledServices.size(); i < count; i++) {
             final AccessibilityServiceInfo installedService = mInstalledServices.get(i);
             final ComponentName componentName = ComponentName.unflattenFromString(
                     installedService.getId());
 
-            if (mCrashedServices.contains(componentName)
-                    && !mEnabledServices.contains(componentName)) {
-                // Remove it from mCrashedServices since users toggle the switch bar to retry.
+            if (!mEnabledServices.contains(componentName)) {
+                // Remove from mCrashedServices, since users may toggle the on/off switch to retry.
                 mCrashedServices.remove(componentName);
+                // Remove from mBindingServices, since services can get stuck in the binding state
+                // if binding starts but never finishes. If the service later attempts to finish
+                // binding but it is not in the enabled list then it will exit before initializing;
+                // see AccessibilityServiceConnection#initializeService().
+                mBindingServices.remove(componentName);
             }
         }
     }
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index a185b58..3cfae60 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -104,8 +104,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
@@ -124,6 +122,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.IRemoteViewsFactory;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
 import com.android.server.WidgetBackupProvider;
@@ -872,6 +872,33 @@
     }
 
     @Override
+    public void setAppWidgetHidden(String callingPackage, int hostId) {
+        final int userId = UserHandle.getCallingUserId();
+
+        if (DEBUG) {
+            Slog.i(TAG, "setAppWidgetHidden() " + userId);
+        }
+
+        mSecurityPolicy.enforceCallFromPackage(callingPackage);
+
+        synchronized (mLock) {
+            ensureGroupStateLoadedLocked(userId, /* enforceUserUnlockingOrUnlocked */false);
+
+            HostId id = new HostId(Binder.getCallingUid(), hostId, callingPackage);
+            Host host = lookupHostLocked(id);
+
+            if (host != null) {
+                try {
+                    mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false);
+                } catch (NullPointerException e) {
+                    Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e);
+                    throw e;
+                }
+            }
+        }
+    }
+
+    @Override
     public void deleteAppWidgetId(String callingPackage, int appWidgetId) {
         final int userId = UserHandle.getCallingUserId();
 
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
index 92435d0..3ea1bcb 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
@@ -23,8 +23,9 @@
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.IOException;
 import java.util.Objects;
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index eac98f2..f0492a8 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -376,6 +376,16 @@
                 return;
             }
 
+            // In some cases there may not be a monitor passed in when creating this task. So, if we
+            // don't have one already we ask the transport for a monitor.
+            if (mMonitor == null) {
+                try {
+                    mMonitor = transport.getBackupManagerMonitor();
+                } catch (RemoteException e) {
+                    Slog.i(TAG, "Failed to retrieve monitor from transport");
+                }
+            }
+
             // Set up to send data to the transport
             final int N = mPackages.size();
             final byte[] buffer = new byte[8192];
diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
index 03796ea..95cc289 100644
--- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java
+++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java
@@ -21,6 +21,7 @@
 import static com.android.server.backup.BackupManagerService.TAG;
 
 import android.app.backup.BackupManager.OperationType;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreSet;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -203,6 +204,14 @@
                     }
                 }
 
+                // Ask the transport for a monitor that will be used to relay log events back to it.
+                IBackupManagerMonitor monitor = null;
+                try {
+                    monitor = transport.getBackupManagerMonitor();
+                } catch (RemoteException e) {
+                    Slog.i(TAG, "Failed to retrieve monitor from transport");
+                }
+
                 // At this point, we have started a new journal file, and the old
                 // file identity is being passed to the backup processing task.
                 // When it completes successfully, that old journal file will be
@@ -225,7 +234,7 @@
                                 queue,
                                 oldJournal,
                                 /* observer */ null,
-                                /* monitor */ null,
+                                monitor,
                                 listener,
                                 Collections.emptyList(),
                                 /* userInitiated */ false,
diff --git a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
index 237a3fa..40d7cad 100644
--- a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -18,10 +18,12 @@
 
 import android.annotation.Nullable;
 import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -363,6 +365,15 @@
     }
 
     /**
+     * See {@link IBackupTransport#getBackupManagerMonitor()}
+     */
+    public IBackupManagerMonitor getBackupManagerMonitor() throws RemoteException {
+        AndroidFuture<IBackupManagerMonitor> resultFuture = mTransportFutures.newFuture();
+        mTransportBinder.getBackupManagerMonitor(resultFuture);
+        return IBackupManagerMonitor.Stub.asInterface((IBinder) getFutureResult(resultFuture));
+    }
+
+    /**
      * Allows the {@link TransportConnection} to notify this client
      * if the underlying transport has become unusable.  If that happens
      * we want to cancel all active futures or callbacks.
diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING
index 38d9372..37c47ba 100644
--- a/services/companion/TEST_MAPPING
+++ b/services/companion/TEST_MAPPING
@@ -8,14 +8,6 @@
     },
     {
       "name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases"
-    },
-    {
-      "name": "CtsOsTestCases",
-      "options": [
-        {
-          "include-filter": "android.os.cts.CompanionDeviceManagerTest"
-        }
-      ]
     }
   ]
 }
diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java
index c4f5766..a57f5a2 100644
--- a/services/companion/java/com/android/server/companion/PersistentDataStore.java
+++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java
@@ -45,11 +45,11 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
index dbff628..2c5d582 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferRequestStore.java
@@ -35,12 +35,12 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
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 ec30369b..02053cc 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -31,6 +31,7 @@
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.IInputConstants;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Slog;
@@ -75,7 +76,7 @@
     @interface PhysType {
     }
 
-    private final Object mLock;
+    final Object mLock;
 
     /* Token -> file descriptor associations. */
     @VisibleForTesting
@@ -220,6 +221,19 @@
         }
     }
 
+    /**
+     * @return the device id for a given token (identifiying a device)
+     */
+    int getInputDeviceId(IBinder token) {
+        synchronized (mLock) {
+            final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(token);
+            if (inputDeviceDescriptor == null) {
+                throw new IllegalArgumentException("Could not get device id for given token");
+            }
+            return inputDeviceDescriptor.getInputDeviceId();
+        }
+    }
+
     void setShowPointerIcon(boolean visible, int displayId) {
         mInputManagerInternal.setPointerIconVisible(visible, displayId);
     }
@@ -393,10 +407,22 @@
                         + inputDeviceDescriptor.getCreationOrderNumber());
                 fout.println("          type: " + inputDeviceDescriptor.getType());
                 fout.println("          phys: " + inputDeviceDescriptor.getPhys());
+                fout.println(
+                        "          inputDeviceId: " + inputDeviceDescriptor.getInputDeviceId());
             }
         }
     }
 
+    @VisibleForTesting
+    void addDeviceForTesting(IBinder deviceToken, int fd, int type, int displayId,
+            String phys, int inputDeviceId) {
+        synchronized (mLock) {
+            mInputDeviceDescriptors.put(deviceToken,
+                    new InputDeviceDescriptor(fd, () -> {}, type, displayId, phys,
+                            inputDeviceId));
+        }
+    }
+
     private static native int nativeOpenUinputDpad(String deviceName, int vendorId,
             int productId, String phys);
     private static native int nativeOpenUinputKeyboard(String deviceName, int vendorId,
@@ -493,16 +519,20 @@
         private final @Type int mType;
         private final int mDisplayId;
         private final String mPhys;
+        // The input device id that was associated to the device by the InputReader on device
+        // creation.
+        private final int mInputDeviceId;
         // Monotonically increasing number; devices with lower numbers were created earlier.
         private final long mCreationOrderNumber;
 
         InputDeviceDescriptor(int fd, IBinder.DeathRecipient deathRecipient, @Type int type,
-                int displayId, String phys) {
+                int displayId, String phys, int inputDeviceId) {
             mFd = fd;
             mDeathRecipient = deathRecipient;
             mType = type;
             mDisplayId = displayId;
             mPhys = phys;
+            mInputDeviceId = inputDeviceId;
             mCreationOrderNumber = sNextCreationOrderNumber.getAndIncrement();
         }
 
@@ -533,6 +563,10 @@
         public String getPhys() {
             return mPhys;
         }
+
+        public int getInputDeviceId() {
+            return mInputDeviceId;
+        }
     }
 
     private final class BinderDeathRecipient implements IBinder.DeathRecipient {
@@ -558,6 +592,8 @@
         private final CountDownLatch mDeviceAddedLatch = new CountDownLatch(1);
         private final InputManager.InputDeviceListener mListener;
 
+        private int mInputDeviceId = IInputConstants.INVALID_INPUT_DEVICE_ID;
+
         WaitForDevice(String deviceName, int vendorId, int productId) {
             mListener = new InputManager.InputDeviceListener() {
                 @Override
@@ -572,6 +608,7 @@
                     if (id.getVendorId() != vendorId || id.getProductId() != productId) {
                         return;
                     }
+                    mInputDeviceId = deviceId;
                     mDeviceAddedLatch.countDown();
                 }
 
@@ -588,8 +625,13 @@
             InputManager.getInstance().registerInputDeviceListener(mListener, mHandler);
         }
 
-        /** Note: This must not be called from {@link #mHandler}'s thread. */
-        void waitForDeviceCreation() throws DeviceCreationException {
+        /**
+         * Note: This must not be called from {@link #mHandler}'s thread.
+         * @throws DeviceCreationException if the device was not created successfully within the
+         * timeout.
+         * @return The id of the created input device.
+         */
+        int waitForDeviceCreation() throws DeviceCreationException {
             try {
                 if (!mDeviceAddedLatch.await(1, TimeUnit.MINUTES)) {
                     throw new DeviceCreationException(
@@ -599,6 +641,12 @@
                 throw new DeviceCreationException(
                         "Interrupted while waiting for virtual device to be created.", e);
             }
+            if (mInputDeviceId == IInputConstants.INVALID_INPUT_DEVICE_ID) {
+                throw new IllegalStateException(
+                        "Virtual input device was created with an invalid "
+                                + "id=" + mInputDeviceId);
+            }
+            return mInputDeviceId;
         }
 
         @Override
@@ -643,6 +691,8 @@
         final int fd;
         final BinderDeathRecipient binderDeathRecipient;
 
+        final int inputDeviceId;
+
         setUniqueIdAssociation(displayId, phys);
         try (WaitForDevice waiter = new WaitForDevice(deviceName, vendorId, productId)) {
             fd = deviceOpener.get();
@@ -652,7 +702,7 @@
             }
             // The fd is valid from here, so ensure that all failures close the fd after this point.
             try {
-                waiter.waitForDeviceCreation();
+                inputDeviceId = waiter.waitForDeviceCreation();
 
                 binderDeathRecipient = new BinderDeathRecipient(deviceToken);
                 try {
@@ -672,7 +722,8 @@
 
         synchronized (mLock) {
             mInputDeviceDescriptors.put(deviceToken,
-                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys));
+                    new InputDeviceDescriptor(fd, binderDeathRecipient, type, displayId, phys,
+                            inputDeviceId));
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 2835b69..5ebbf07 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -498,6 +498,17 @@
     }
 
     @Override // Binder call
+    public int getInputDeviceId(IBinder token) {
+        final long binderToken = Binder.clearCallingIdentity();
+        try {
+            return mInputController.getInputDeviceId(token);
+        } finally {
+            Binder.restoreCallingIdentity(binderToken);
+        }
+    }
+
+
+    @Override // Binder call
     public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
         final long binderToken = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index c713a41..551ffff 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -38,8 +38,6 @@
 import android.util.AtomicFile;
 import android.util.EventLog;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -47,6 +45,8 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.am.DropboxRateLimiter;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/ContextHubSystemService.java b/services/core/java/com/android/server/ContextHubSystemService.java
index 96ff900..e6e83e0 100644
--- a/services/core/java/com/android/server/ContextHubSystemService.java
+++ b/services/core/java/com/android/server/ContextHubSystemService.java
@@ -23,6 +23,7 @@
 
 import com.android.internal.util.ConcurrentUtils;
 import com.android.server.location.contexthub.ContextHubService;
+import com.android.server.location.contexthub.IContextHubWrapper;
 
 import java.util.concurrent.Future;
 
@@ -35,7 +36,8 @@
     public ContextHubSystemService(Context context) {
         super(context);
         mInit = SystemServerInitThreadPool.submit(() -> {
-            mContextHubService = new ContextHubService(context);
+            mContextHubService = new ContextHubService(context,
+                    IContextHubWrapper.getContextHubWrapper());
         }, "Init ContextHubSystemService");
     }
 
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 960922e..92889c0 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -40,8 +40,6 @@
 import android.util.LongArrayQueue;
 import android.util.MathUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -49,6 +47,8 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 0cf7915..72876f6 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -131,8 +131,6 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -147,6 +145,8 @@
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Installer;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.storage.AppFuseBridge;
@@ -3584,6 +3584,13 @@
         final boolean includeSharedProfile =
                 (flags & StorageManager.FLAG_INCLUDE_SHARED_PROFILE) != 0;
 
+        // When the caller is the app actually hosting external storage, we
+        // should never attempt to augment the actual storage volume state,
+        // otherwise we risk confusing it with race conditions as users go
+        // through various unlocked states
+        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
+                mMediaStoreAuthorityAppId);
+
         // Only Apps with MANAGE_EXTERNAL_STORAGE should call the API with includeSharedProfile
         if (includeSharedProfile) {
             try {
@@ -3596,8 +3603,13 @@
                 // Checking first entry in packagesFromUid is enough as using "sharedUserId"
                 // mechanism is rare and discouraged. Also, Apps that share same UID share the same
                 // permissions.
-                if (!mStorageManagerInternal.hasExternalStorageAccess(callingUid,
-                        packagesFromUid[0])) {
+                // Allowing Media Provider is an exception, Media Provider process should be allowed
+                // to query users across profiles, even without MANAGE_EXTERNAL_STORAGE access.
+                // Note that ordinarily Media provider process has the above permission, but if they
+                // are revoked, Storage Volume(s) should still be returned.
+                if (!callerIsMediaStore
+                        && !mStorageManagerInternal.hasExternalStorageAccess(callingUid,
+                                packagesFromUid[0])) {
                     throw new SecurityException("Only File Manager Apps permitted");
                 }
             } catch (RemoteException re) {
@@ -3610,13 +3622,6 @@
         // point
         final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM);
 
-        // When the caller is the app actually hosting external storage, we
-        // should never attempt to augment the actual storage volume state,
-        // otherwise we risk confusing it with race conditions as users go
-        // through various unlocked states
-        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
-                mMediaStoreAuthorityAppId);
-
         final boolean userIsDemo;
         final boolean userKeyUnlocked;
         final boolean storagePermission;
diff --git a/services/core/java/com/android/server/SystemUpdateManagerService.java b/services/core/java/com/android/server/SystemUpdateManagerService.java
index fcba9b5..811a780 100644
--- a/services/core/java/com/android/server/SystemUpdateManagerService.java
+++ b/services/core/java/com/android/server/SystemUpdateManagerService.java
@@ -38,12 +38,12 @@
 import android.provider.Settings;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index f7833b0..2652ebe 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2581,33 +2581,39 @@
         if (!checkNotifyPermission("notifyBarringInfo()")) {
             return;
         }
-        if (barringInfo == null) {
-            log("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
-            mBarringInfo.set(phoneId, new BarringInfo());
+        if (!validatePhoneId(phoneId)) {
+            loge("Received invalid phoneId for BarringInfo = " + phoneId);
             return;
         }
 
         synchronized (mRecords) {
-            if (validatePhoneId(phoneId)) {
-                mBarringInfo.set(phoneId, barringInfo);
-                // Barring info is non-null
-                BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
-                if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
-                for (Record r : mRecords) {
-                    if (r.matchTelephonyCallbackEvent(
-                            TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
-                            && idMatch(r, subId, phoneId)) {
-                        try {
-                            if (DBG_LOC) {
-                                log("notifyBarringInfo: mBarringInfo="
-                                        + barringInfo + " r=" + r);
-                            }
-                            r.callback.onBarringInfoChanged(
-                                    checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
-                                        ? barringInfo : biNoLocation);
-                        } catch (RemoteException ex) {
-                            mRemoveList.add(r.binder);
+            if (barringInfo == null) {
+                loge("Received null BarringInfo for subId=" + subId + ", phoneId=" + phoneId);
+                mBarringInfo.set(phoneId, new BarringInfo());
+                return;
+            }
+            if (barringInfo.equals(mBarringInfo.get(phoneId))) {
+                if (VDBG) log("Ignoring duplicate barring info.");
+                return;
+            }
+            mBarringInfo.set(phoneId, barringInfo);
+            // Barring info is non-null
+            BarringInfo biNoLocation = barringInfo.createLocationInfoSanitizedCopy();
+            if (VDBG) log("listen: call onBarringInfoChanged=" + barringInfo);
+            for (Record r : mRecords) {
+                if (r.matchTelephonyCallbackEvent(
+                        TelephonyCallback.EVENT_BARRING_INFO_CHANGED)
+                        && idMatch(r, subId, phoneId)) {
+                    try {
+                        if (DBG_LOC) {
+                            log("notifyBarringInfo: mBarringInfo="
+                                    + barringInfo + " r=" + r);
                         }
+                        r.callback.onBarringInfoChanged(
+                                checkFineLocationAccess(r, Build.VERSION_CODES.BASE)
+                                    ? barringInfo : biNoLocation);
+                    } catch (RemoteException ex) {
+                        mRemoveList.add(r.binder);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java b/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java
index 725bccf..12f2e10 100644
--- a/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java
+++ b/services/core/java/com/android/server/accounts/AccountAuthenticatorCache.java
@@ -27,8 +27,9 @@
 import android.content.res.TypedArray;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java b/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java
index b379b5d..3603dcd 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerBackupHelper.java
@@ -29,13 +29,13 @@
 import android.util.PackageUtils;
 import android.util.Pair;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 04bd869..62a97dc 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -65,8 +65,6 @@
 import android.util.AtomicFile;
 import android.util.Base64;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
@@ -75,6 +73,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.FgThread;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7a09109..b166adc 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -3967,8 +3967,12 @@
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
+        final boolean hasKillAllPermission = checkCallingPermission(
+                android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES) == PERMISSION_GRANTED;
+        final int callingUid = Binder.getCallingUid();
+        final int callingAppId = UserHandle.getAppId(callingUid);
 
-        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+        userId = mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid,
                 userId, true, ALLOW_FULL_ONLY, "killBackgroundProcesses", null);
         final int[] userIds = mUserController.expandUserId(userId);
 
@@ -3983,7 +3987,7 @@
                                     targetUserId));
                 } catch (RemoteException e) {
                 }
-                if (appId == -1) {
+                if (appId == -1 || (!hasKillAllPermission && appId != callingAppId)) {
                     Slog.w(TAG, "Invalid packageName: " + packageName);
                     return;
                 }
@@ -4002,11 +4006,11 @@
 
     @Override
     public void killAllBackgroundProcesses() {
-        if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
+        if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
             final String msg = "Permission Denial: killAllBackgroundProcesses() from pid="
                     + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
-                    + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
+                    + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
@@ -4042,11 +4046,11 @@
      *                     processes, or {@code -1} to ignore the process state
      */
     void killAllBackgroundProcessesExcept(int minTargetSdk, int maxProcState) {
-        if (checkCallingPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES)
+        if (checkCallingPermission(android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES)
                 != PackageManager.PERMISSION_GRANTED) {
             final String msg = "Permission Denial: killAllBackgroundProcessesExcept() from pid="
                     + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
-                    + " requires " + android.Manifest.permission.KILL_BACKGROUND_PROCESSES;
+                    + " requires " + android.Manifest.permission.KILL_ALL_BACKGROUND_PROCESSES;
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
@@ -13373,27 +13377,19 @@
         int callingPid;
         boolean instantApp;
         synchronized(this) {
-            if (caller != null) {
-                callerApp = getRecordForAppLOSP(caller);
-                if (callerApp == null) {
-                    throw new SecurityException(
-                            "Unable to find app for caller " + caller
-                            + " (pid=" + Binder.getCallingPid()
-                            + ") when registering receiver " + receiver);
-                }
-                if (callerApp.info.uid != SYSTEM_UID
-                        && !callerApp.getPkgList().containsKey(callerPackage)
-                        && !"android".equals(callerPackage)) {
-                    throw new SecurityException("Given caller package " + callerPackage
-                            + " is not running in process " + callerApp);
-                }
-                callingUid = callerApp.info.uid;
-                callingPid = callerApp.getPid();
-            } else {
-                callerPackage = null;
-                callingUid = Binder.getCallingUid();
-                callingPid = Binder.getCallingPid();
+            callerApp = getRecordForAppLOSP(caller);
+            if (callerApp == null) {
+                Slog.w(TAG, "registerReceiverWithFeature: no app for " + caller);
+                return null;
             }
+            if (callerApp.info.uid != SYSTEM_UID
+                    && !callerApp.getPkgList().containsKey(callerPackage)
+                    && !"android".equals(callerPackage)) {
+                throw new SecurityException("Given caller package " + callerPackage
+                        + " is not running in process " + callerApp);
+            }
+            callingUid = callerApp.info.uid;
+            callingPid = callerApp.getPid();
 
             instantApp = isInstantApp(callerApp, callerPackage, callingUid);
             userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
@@ -13901,7 +13897,7 @@
         if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST,
                 (sticky ? "Broadcast sticky: ": "Broadcast: ") + intent
                 + " ordered=" + ordered + " userid=" + userId);
-        if ((resultTo != null) && !ordered) {
+        if ((resultTo != null) && !ordered && !mEnableModernQueue) {
             Slog.w(TAG, "Broadcast " + intent + " not ordered but result callback requested!");
         }
 
@@ -14463,10 +14459,12 @@
         filterNonExportedComponents(intent, callingUid, registeredReceivers,
                 mPlatformCompat, callerPackage);
         int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
-        if (!ordered && NR > 0) {
+        if (!ordered && NR > 0 && !mEnableModernQueue) {
             // If we are not serializing this broadcast, then send the
             // registered receivers separately so they don't wait for the
-            // components to be launched.
+            // components to be launched. We don't do this split for the modern
+            // queue because delivery to registered receivers isn't blocked
+            // behind manifest receivers.
             if (isCallerSystem) {
                 checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
                         isProtectedBroadcast, registeredReceivers);
@@ -14700,13 +14698,14 @@
             // Non-system callers can't declare that a broadcast is alarm-related.
             // The PendingIntent invocation case is handled in PendingIntentRecord.
             if (bOptions != null && callingUid != SYSTEM_UID) {
-                if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+                if (bOptions.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)
+                        || bOptions.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) {
                     if (DEBUG_BROADCAST) {
                         Slog.w(TAG, "Non-system caller " + callingUid
-                                + " may not flag broadcast as alarm-related");
+                                + " may not flag broadcast as alarm or interactive");
                     }
                     throw new SecurityException(
-                            "Non-system callers may not flag broadcasts as alarm-related");
+                            "Non-system callers may not flag broadcasts as alarm or interactive");
                 }
             }
 
@@ -16898,6 +16897,11 @@
             return mSystemReady;
         }
 
+        @Override
+        public boolean isModernQueueEnabled() {
+            return mEnableModernQueue;
+        }
+
         /**
          * Returns package name by pid.
          */
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index e0690bf..ba1c3b3 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -146,8 +146,6 @@
 import android.util.SparseArray;
 import android.util.SparseArrayMap;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -157,6 +155,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.function.TriConsumer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.AppStateTracker;
 import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index a4a1c2f..4590c85 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -167,7 +167,7 @@
      */
     public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS;
     private static final String KEY_DELAY_NORMAL_MILLIS = "bcast_delay_normal_millis";
-    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 10_000 * Build.HW_TIMEOUT_MULTIPLIER;
+    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 0;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Delay to apply to broadcasts
@@ -175,7 +175,16 @@
      */
     public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS;
     private static final String KEY_DELAY_CACHED_MILLIS = "bcast_delay_cached_millis";
-    private static final long DEFAULT_DELAY_CACHED_MILLIS = 30_000 * Build.HW_TIMEOUT_MULTIPLIER;
+    private static final long DEFAULT_DELAY_CACHED_MILLIS = 0;
+
+    /**
+     * For {@link BroadcastQueueModernImpl}: Delay to apply to urgent
+     * broadcasts, typically a negative value to indicate they should be
+     * executed before most other pending broadcasts.
+     */
+    public long DELAY_URGENT_MILLIS = DEFAULT_DELAY_URGENT_MILLIS;
+    private static final String KEY_DELAY_URGENT_MILLIS = "bcast_delay_urgent_millis";
+    private static final long DEFAULT_DELAY_URGENT_MILLIS = -120_000;
 
     /**
      * For {@link BroadcastQueueModernImpl}: Maximum number of complete
@@ -313,6 +322,8 @@
                     DEFAULT_DELAY_NORMAL_MILLIS);
             DELAY_CACHED_MILLIS = getDeviceConfigLong(KEY_DELAY_CACHED_MILLIS,
                     DEFAULT_DELAY_CACHED_MILLIS);
+            DELAY_URGENT_MILLIS = getDeviceConfigLong(KEY_DELAY_URGENT_MILLIS,
+                    DEFAULT_DELAY_URGENT_MILLIS);
             MAX_HISTORY_COMPLETE_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_COMPLETE_SIZE,
                     DEFAULT_MAX_HISTORY_COMPLETE_SIZE);
             MAX_HISTORY_SUMMARY_SIZE = getDeviceConfigInt(KEY_MAX_HISTORY_SUMMARY_SIZE,
@@ -354,6 +365,8 @@
                     TimeUtils.formatDuration(DELAY_NORMAL_MILLIS)).println();
             pw.print(KEY_DELAY_CACHED_MILLIS,
                     TimeUtils.formatDuration(DELAY_CACHED_MILLIS)).println();
+            pw.print(KEY_DELAY_URGENT_MILLIS,
+                    TimeUtils.formatDuration(DELAY_URGENT_MILLIS)).println();
             pw.print(KEY_MAX_HISTORY_COMPLETE_SIZE, MAX_HISTORY_COMPLETE_SIZE).println();
             pw.print(KEY_MAX_HISTORY_SUMMARY_SIZE, MAX_HISTORY_SUMMARY_SIZE).println();
             pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 0d6ac1d..5123517 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -103,6 +103,13 @@
     private final ArrayDeque<SomeArgs> mPending = new ArrayDeque<>();
 
     /**
+     * Ordered collection of "urgent" broadcasts that are waiting to be
+     * dispatched to this process, in the same representation as
+     * {@link #mPending}.
+     */
+    private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>();
+
+    /**
      * Broadcast actively being dispatched to this process.
      */
     private @Nullable BroadcastRecord mActive;
@@ -140,12 +147,16 @@
     private int mCountOrdered;
     private int mCountAlarm;
     private int mCountPrioritized;
+    private int mCountInteractive;
+    private int mCountResultTo;
+    private int mCountInstrumented;
 
     private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE;
     private @Reason int mRunnableAtReason = REASON_EMPTY;
     private boolean mRunnableAtInvalidated;
 
     private boolean mProcessCached;
+    private boolean mProcessInstrumented;
 
     private String mCachedToString;
     private String mCachedToShortString;
@@ -172,40 +183,65 @@
      */
     public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
             int blockedUntilTerminalCount) {
-        // If caller wants to replace, walk backwards looking for any matches
         if (record.isReplacePending()) {
-            final Iterator<SomeArgs> it = mPending.descendingIterator();
-            final Object receiver = record.receivers.get(recordIndex);
-            while (it.hasNext()) {
-                final SomeArgs args = it.next();
-                final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
-                final Object testReceiver = testRecord.receivers.get(args.argi1);
-                if ((record.callingUid == testRecord.callingUid)
-                        && (record.userId == testRecord.userId)
-                        && record.intent.filterEquals(testRecord.intent)
-                        && isReceiverEquals(receiver, testReceiver)) {
-                    // Exact match found; perform in-place swap
-                    args.arg1 = record;
-                    args.argi1 = recordIndex;
-                    args.argi2 = blockedUntilTerminalCount;
-                    onBroadcastDequeued(testRecord);
-                    onBroadcastEnqueued(record);
-                    return;
-                }
+            boolean didReplace = replaceBroadcastInQueue(mPending,
+                    record, recordIndex, blockedUntilTerminalCount)
+                    || replaceBroadcastInQueue(mPendingUrgent,
+                    record, recordIndex, blockedUntilTerminalCount);
+            if (didReplace) {
+                return;
             }
         }
 
         // Caller isn't interested in replacing, or we didn't find any pending
         // item to replace above, so enqueue as a new broadcast
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = record;
-        args.argi1 = recordIndex;
-        args.argi2 = blockedUntilTerminalCount;
-        mPending.addLast(args);
+        SomeArgs newBroadcastArgs = SomeArgs.obtain();
+        newBroadcastArgs.arg1 = record;
+        newBroadcastArgs.argi1 = recordIndex;
+        newBroadcastArgs.argi2 = blockedUntilTerminalCount;
+
+        // Cross-broadcast prioritization policy:  some broadcasts might warrant being
+        // issued ahead of others that are already pending, for example if this new
+        // broadcast is in a different delivery class or is tied to a direct user interaction
+        // with implicit responsiveness expectations.
+        final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending;
+        queue.addLast(newBroadcastArgs);
         onBroadcastEnqueued(record);
     }
 
     /**
+     * Searches from newest to oldest, and at the first matching pending broadcast
+     * it finds, replaces it in-place and returns -- does not attempt to handle
+     * "duplicate" broadcasts in the queue.
+     * <p>
+     * @return {@code true} if it found and replaced an existing record in the queue;
+     * {@code false} otherwise.
+     */
+    private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
+            @NonNull BroadcastRecord record, int recordIndex,  int blockedUntilTerminalCount) {
+        final Iterator<SomeArgs> it = queue.descendingIterator();
+        final Object receiver = record.receivers.get(recordIndex);
+        while (it.hasNext()) {
+            final SomeArgs args = it.next();
+            final BroadcastRecord testRecord = (BroadcastRecord) args.arg1;
+            final Object testReceiver = testRecord.receivers.get(args.argi1);
+            if ((record.callingUid == testRecord.callingUid)
+                    && (record.userId == testRecord.userId)
+                    && record.intent.filterEquals(testRecord.intent)
+                    && isReceiverEquals(receiver, testReceiver)) {
+                // Exact match found; perform in-place swap
+                args.arg1 = record;
+                args.argi1 = recordIndex;
+                args.argi2 = blockedUntilTerminalCount;
+                onBroadcastDequeued(testRecord);
+                onBroadcastEnqueued(record);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Functional interface that tests a {@link BroadcastRecord} that has been
      * previously enqueued in {@link BroadcastProcessQueue}.
      */
@@ -233,8 +269,18 @@
      */
     public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate,
             @NonNull BroadcastConsumer consumer, boolean andRemove) {
+        boolean didSomething = forEachMatchingBroadcastInQueue(mPending,
+                predicate, consumer, andRemove);
+        didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent,
+                predicate, consumer, andRemove);
+        return didSomething;
+    }
+
+    private boolean forEachMatchingBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
+            @NonNull BroadcastPredicate predicate, @NonNull BroadcastConsumer consumer,
+            boolean andRemove) {
         boolean didSomething = false;
-        final Iterator<SomeArgs> it = mPending.iterator();
+        final Iterator<SomeArgs> it = queue.iterator();
         while (it.hasNext()) {
             final SomeArgs args = it.next();
             final BroadcastRecord record = (BroadcastRecord) args.arg1;
@@ -255,6 +301,18 @@
     }
 
     /**
+     * Update the actively running "warm" process for this process.
+     */
+    public void setProcess(@Nullable ProcessRecord app) {
+        this.app = app;
+        if (app != null) {
+            setProcessInstrumented(app.getActiveInstrumentation() != null);
+        } else {
+            setProcessInstrumented(false);
+        }
+    }
+
+    /**
      * Update if this process is in the "cached" state, typically signaling that
      * broadcast dispatch should be paused or delayed.
      */
@@ -266,6 +324,18 @@
     }
 
     /**
+     * Update if this process is in the "instrumented" state, typically
+     * signaling that broadcast dispatch should bypass all pauses or delays, to
+     * avoid holding up test suites.
+     */
+    public void setProcessInstrumented(boolean instrumented) {
+        if (mProcessInstrumented != instrumented) {
+            mProcessInstrumented = instrumented;
+            invalidateRunnableAt();
+        }
+    }
+
+    /**
      * Return if we know of an actively running "warm" process for this queue.
      */
     public boolean isProcessWarm() {
@@ -273,13 +343,12 @@
     }
 
     public int getPreferredSchedulingGroupLocked() {
-        if (mCountForeground > 0 || mCountOrdered > 0 || mCountAlarm > 0) {
-            // We have an important broadcast somewhere down the queue, so
+        if (mCountForeground > 0) {
+            // We have a foreground broadcast somewhere down the queue, so
             // boost priority until we drain them all
             return ProcessList.SCHED_GROUP_DEFAULT;
-        } else if ((mActive != null)
-                && (mActive.isForeground() || mActive.ordered || mActive.alarm)) {
-            // We have an important broadcast right now, so boost priority
+        } else if ((mActive != null) && mActive.isForeground()) {
+            // We have a foreground broadcast right now, so boost priority
             return ProcessList.SCHED_GROUP_DEFAULT;
         } else if (!isIdle()) {
             return ProcessList.SCHED_GROUP_BACKGROUND;
@@ -309,7 +378,7 @@
      */
     public void makeActiveNextPending() {
         // TODO: what if the next broadcast isn't runnable yet?
-        final SomeArgs next = mPending.removeFirst();
+        final SomeArgs next = removeNextBroadcast();
         mActive = (BroadcastRecord) next.arg1;
         mActiveIndex = next.argi1;
         mActiveBlockedUntilTerminalCount = next.argi2;
@@ -347,6 +416,15 @@
         if (record.prioritized) {
             mCountPrioritized++;
         }
+        if (record.interactive) {
+            mCountInteractive++;
+        }
+        if (record.resultTo != null) {
+            mCountResultTo++;
+        }
+        if (record.callerInstrumented) {
+            mCountInstrumented++;
+        }
         invalidateRunnableAt();
     }
 
@@ -366,6 +444,15 @@
         if (record.prioritized) {
             mCountPrioritized--;
         }
+        if (record.interactive) {
+            mCountInteractive--;
+        }
+        if (record.resultTo != null) {
+            mCountResultTo--;
+        }
+        if (record.callerInstrumented) {
+            mCountInstrumented--;
+        }
         invalidateRunnableAt();
     }
 
@@ -413,7 +500,7 @@
     }
 
     public boolean isEmpty() {
-        return mPending.isEmpty();
+        return mPending.isEmpty() && mPendingUrgent.isEmpty();
     }
 
     public boolean isActive() {
@@ -421,6 +508,38 @@
     }
 
     /**
+     * Will thrown an exception if there are no pending broadcasts; relies on
+     * {@link #isEmpty()} being false.
+     */
+    SomeArgs removeNextBroadcast() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return queue.removeFirst();
+    }
+
+    @Nullable ArrayDeque<SomeArgs> queueForNextBroadcast() {
+        if (!mPendingUrgent.isEmpty()) {
+            return mPendingUrgent;
+        } else if (!mPending.isEmpty()) {
+            return mPending;
+        }
+        return null;
+    }
+
+    /**
+     * Returns null if there are no pending broadcasts
+     */
+    @Nullable SomeArgs peekNextBroadcast() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return (queue != null) ? queue.peekFirst() : null;
+    }
+
+    @VisibleForTesting
+    @Nullable BroadcastRecord peekNextBroadcastRecord() {
+        ArrayDeque<SomeArgs> queue = queueForNextBroadcast();
+        return (queue != null) ? (BroadcastRecord) queue.peekFirst().arg1 : null;
+    }
+
+    /**
      * Quickly determine if this queue has broadcasts that are still waiting to
      * be delivered at some point in the future.
      */
@@ -437,11 +556,13 @@
             return mActive.enqueueTime > barrierTime;
         }
         final SomeArgs next = mPending.peekFirst();
-        if (next != null) {
-            return ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
-        }
-        // Nothing running or runnable means we're past the barrier
-        return true;
+        final SomeArgs nextUrgent = mPendingUrgent.peekFirst();
+        // Empty queue is past any barrier
+        final boolean nextLater = next == null
+                || ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
+        final boolean nextUrgentLater = nextUrgent == null
+                || ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime;
+        return nextLater && nextUrgentLater;
     }
 
     public boolean isRunnable() {
@@ -477,25 +598,33 @@
     }
 
     static final int REASON_EMPTY = 0;
-    static final int REASON_CONTAINS_FOREGROUND = 1;
-    static final int REASON_CONTAINS_ORDERED = 2;
-    static final int REASON_CONTAINS_ALARM = 3;
-    static final int REASON_CONTAINS_PRIORITIZED = 4;
-    static final int REASON_CACHED = 5;
-    static final int REASON_NORMAL = 6;
-    static final int REASON_MAX_PENDING = 7;
-    static final int REASON_BLOCKED = 8;
+    static final int REASON_CACHED = 1;
+    static final int REASON_NORMAL = 2;
+    static final int REASON_MAX_PENDING = 3;
+    static final int REASON_BLOCKED = 4;
+    static final int REASON_INSTRUMENTED = 5;
+    static final int REASON_CONTAINS_FOREGROUND = 10;
+    static final int REASON_CONTAINS_ORDERED = 11;
+    static final int REASON_CONTAINS_ALARM = 12;
+    static final int REASON_CONTAINS_PRIORITIZED = 13;
+    static final int REASON_CONTAINS_INTERACTIVE = 14;
+    static final int REASON_CONTAINS_RESULT_TO = 15;
+    static final int REASON_CONTAINS_INSTRUMENTED = 16;
 
     @IntDef(flag = false, prefix = { "REASON_" }, value = {
             REASON_EMPTY,
-            REASON_CONTAINS_FOREGROUND,
-            REASON_CONTAINS_ORDERED,
-            REASON_CONTAINS_ALARM,
-            REASON_CONTAINS_PRIORITIZED,
             REASON_CACHED,
             REASON_NORMAL,
             REASON_MAX_PENDING,
             REASON_BLOCKED,
+            REASON_INSTRUMENTED,
+            REASON_CONTAINS_FOREGROUND,
+            REASON_CONTAINS_ORDERED,
+            REASON_CONTAINS_ALARM,
+            REASON_CONTAINS_PRIORITIZED,
+            REASON_CONTAINS_INTERACTIVE,
+            REASON_CONTAINS_RESULT_TO,
+            REASON_CONTAINS_INSTRUMENTED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Reason {}
@@ -503,14 +632,18 @@
     static @NonNull String reasonToString(@Reason int reason) {
         switch (reason) {
             case REASON_EMPTY: return "EMPTY";
-            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
-            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
-            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
-            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
             case REASON_CACHED: return "CACHED";
             case REASON_NORMAL: return "NORMAL";
             case REASON_MAX_PENDING: return "MAX_PENDING";
             case REASON_BLOCKED: return "BLOCKED";
+            case REASON_INSTRUMENTED: return "INSTRUMENTED";
+            case REASON_CONTAINS_FOREGROUND: return "CONTAINS_FOREGROUND";
+            case REASON_CONTAINS_ORDERED: return "CONTAINS_ORDERED";
+            case REASON_CONTAINS_ALARM: return "CONTAINS_ALARM";
+            case REASON_CONTAINS_PRIORITIZED: return "CONTAINS_PRIORITIZED";
+            case REASON_CONTAINS_INTERACTIVE: return "CONTAINS_INTERACTIVE";
+            case REASON_CONTAINS_RESULT_TO: return "CONTAINS_RESULT_TO";
+            case REASON_CONTAINS_INSTRUMENTED: return "CONTAINS_INSTRUMENTED";
             default: return Integer.toString(reason);
         }
     }
@@ -519,7 +652,7 @@
      * Update {@link #getRunnableAt()} if it's currently invalidated.
      */
     private void updateRunnableAt() {
-        final SomeArgs next = mPending.peekFirst();
+        final SomeArgs next = peekNextBroadcast();
         if (next != null) {
             final BroadcastRecord r = (BroadcastRecord) next.arg1;
             final int index = next.argi1;
@@ -535,17 +668,18 @@
                 return;
             }
 
-            // If we have too many broadcasts pending, bypass any delays that
-            // might have been applied above to aid draining
-            if (mPending.size() >= constants.MAX_PENDING_BROADCASTS) {
-                mRunnableAt = runnableAt;
-                mRunnableAtReason = REASON_MAX_PENDING;
-                return;
-            }
-
             if (mCountForeground > 0) {
-                mRunnableAt = runnableAt;
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
                 mRunnableAtReason = REASON_CONTAINS_FOREGROUND;
+            } else if (mCountInteractive > 0) {
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
+                mRunnableAtReason = REASON_CONTAINS_INTERACTIVE;
+            } else if (mCountInstrumented > 0) {
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
+                mRunnableAtReason = REASON_CONTAINS_INSTRUMENTED;
+            } else if (mProcessInstrumented) {
+                mRunnableAt = runnableAt + constants.DELAY_URGENT_MILLIS;
+                mRunnableAtReason = REASON_INSTRUMENTED;
             } else if (mCountOrdered > 0) {
                 mRunnableAt = runnableAt;
                 mRunnableAtReason = REASON_CONTAINS_ORDERED;
@@ -555,6 +689,9 @@
             } else if (mCountPrioritized > 0) {
                 mRunnableAt = runnableAt;
                 mRunnableAtReason = REASON_CONTAINS_PRIORITIZED;
+            } else if (mCountResultTo > 0) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_CONTAINS_RESULT_TO;
             } else if (mProcessCached) {
                 mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS;
                 mRunnableAtReason = REASON_CACHED;
@@ -562,6 +699,13 @@
                 mRunnableAt = runnableAt + constants.DELAY_NORMAL_MILLIS;
                 mRunnableAtReason = REASON_NORMAL;
             }
+
+            // If we have too many broadcasts pending, bypass any delays that
+            // might have been applied above to aid draining
+            if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) {
+                mRunnableAt = runnableAt;
+                mRunnableAtReason = REASON_MAX_PENDING;
+            }
         } else {
             mRunnableAt = Long.MAX_VALUE;
             mRunnableAtReason = REASON_EMPTY;
@@ -574,8 +718,8 @@
      */
     public void checkHealthLocked() {
         if (mRunnableAtReason == REASON_BLOCKED) {
-            final SomeArgs next = mPending.peekFirst();
-            Objects.requireNonNull(next, "peekFirst");
+            final SomeArgs next = peekNextBroadcast();
+            Objects.requireNonNull(next, "peekNextBroadcast");
 
             // If blocked more than 10 minutes, we're likely wedged
             final BroadcastRecord r = (BroadcastRecord) next.arg1;
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index f34565b..ffc54d9 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -1488,7 +1488,8 @@
             // LocalServices.getService() here.
             final UserManagerInternal umInternal = LocalServices.getService(
                     UserManagerInternal.class);
-            final UserInfo userInfo = umInternal.getUserInfo(r.userId);
+            final UserInfo userInfo =
+                    (umInternal != null) ? umInternal.getUserInfo(r.userId) : null;
             if (userInfo != null) {
                 userType = UserManager.getUserTypeForStatsd(userInfo.userType);
             }
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index e421c61..4c831bd 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -210,6 +210,12 @@
     private final BroadcastConstants mFgConstants;
     private final BroadcastConstants mBgConstants;
 
+    /**
+     * Timestamp when last {@link #testAllProcessQueues} failure was observed;
+     * used for throttling log messages.
+     */
+    private @UptimeMillisLong long mLastTestFailureTime;
+
     private static final int MSG_UPDATE_RUNNING_LIST = 1;
     private static final int MSG_DELIVERY_TIMEOUT_SOFT = 2;
     private static final int MSG_DELIVERY_TIMEOUT_HARD = 3;
@@ -441,7 +447,7 @@
         // relevant per-process queue
         final BroadcastProcessQueue queue = getProcessQueue(app);
         if (queue != null) {
-            queue.app = app;
+            queue.setProcess(app);
         }
 
         boolean didSomething = false;
@@ -478,7 +484,7 @@
         // relevant per-process queue
         final BroadcastProcessQueue queue = getProcessQueue(app);
         if (queue != null) {
-            queue.app = null;
+            queue.setProcess(null);
         }
 
         if ((mRunningColdStart != null) && (mRunningColdStart == queue)) {
@@ -521,6 +527,8 @@
 
     @Override
     public void enqueueBroadcastLocked(@NonNull BroadcastRecord r) {
+        if (DEBUG_BROADCAST) logv("Enqueuing " + r + " for " + r.receivers.size() + " receivers");
+
         r.applySingletonPolicy(mService);
 
         final IntentFilter removeMatchingFilter = (r.options != null)
@@ -816,19 +824,21 @@
         }
 
         final BroadcastRecord r = queue.getActive();
-        r.resultCode = resultCode;
-        r.resultData = resultData;
-        r.resultExtras = resultExtras;
-        if (!r.isNoAbort()) {
-            r.resultAbort = resultAbort;
-        }
+        if (r.ordered) {
+            r.resultCode = resultCode;
+            r.resultData = resultData;
+            r.resultExtras = resultExtras;
+            if (!r.isNoAbort()) {
+                r.resultAbort = resultAbort;
+            }
 
-        // When the caller aborted an ordered broadcast, we mark all remaining
-        // receivers as skipped
-        if (r.ordered && r.resultAbort) {
-            for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
-                setDeliveryState(null, null, r, i, r.receivers.get(i),
-                        BroadcastRecord.DELIVERY_SKIPPED);
+            // When the caller aborted an ordered broadcast, we mark all
+            // remaining receivers as skipped
+            if (r.resultAbort) {
+                for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
+                    setDeliveryState(null, null, r, i, r.receivers.get(i),
+                            BroadcastRecord.DELIVERY_SKIPPED);
+                }
             }
         }
 
@@ -857,10 +867,11 @@
             mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_HARD, queue);
         }
 
-        // Even if we have more broadcasts, if we've made reasonable progress
-        // and someone else is waiting, retire ourselves to avoid starvation
-        final boolean shouldRetire = (mRunnableHead != null)
-                && (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
+        // If we've made reasonable progress, periodically retire ourselves to
+        // avoid starvation of other processes and stack overflow when a
+        // broadcast is immediately finished without waiting
+        final boolean shouldRetire =
+                (queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
 
         if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) {
             // We're on a roll; move onto the next broadcast for this process
@@ -925,7 +936,8 @@
             notifyFinishReceiver(queue, r, index, receiver);
 
             // When entire ordered broadcast finished, deliver final result
-            if (r.ordered && (r.terminalCount == r.receivers.size())) {
+            final boolean recordFinished = (r.terminalCount == r.receivers.size());
+            if (recordFinished) {
                 scheduleResultTo(r);
             }
 
@@ -1031,7 +1043,11 @@
             BroadcastProcessQueue leaf = mProcessQueues.valueAt(i);
             while (leaf != null) {
                 if (!test.test(leaf)) {
-                    logv("Test " + label + " failed due to " + leaf.toShortString(), pw);
+                    final long now = SystemClock.uptimeMillis();
+                    if (now > mLastTestFailureTime + DateUtils.SECOND_IN_MILLIS) {
+                        mLastTestFailureTime = now;
+                        logv("Test " + label + " failed due to " + leaf.toShortString(), pw);
+                    }
                     return false;
                 }
                 leaf = leaf.processNameNext;
@@ -1217,7 +1233,7 @@
 
     private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) {
         if (!queue.isProcessWarm()) {
-            queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid);
+            queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid));
         }
     }
 
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 4f64003..2a3c897 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -78,11 +78,13 @@
     final int callingPid;   // the pid of who sent this
     final int callingUid;   // the uid of who sent this
     final boolean callerInstantApp; // caller is an Instant App?
+    final boolean callerInstrumented; // caller is being instrumented?
     final boolean ordered;  // serialize the send to receivers?
     final boolean sticky;   // originated from existing sticky data?
     final boolean alarm;    // originated from an alarm triggering?
     final boolean pushMessage; // originated from a push message?
     final boolean pushMessageOverQuota; // originated from a push message which was over quota?
+    final boolean interactive; // originated from user interaction?
     final boolean initialSticky; // initial broadcast from register to sticky?
     final boolean prioritized; // contains more than one priority tranche
     final int userId;       // user id this broadcast was for
@@ -364,6 +366,7 @@
         callingPid = _callingPid;
         callingUid = _callingUid;
         callerInstantApp = _callerInstantApp;
+        callerInstrumented = isCallerInstrumented(_callerApp, _callingUid);
         resolvedType = _resolvedType;
         requiredPermissions = _requiredPermissions;
         excludedPermissions = _excludedPermissions;
@@ -392,6 +395,7 @@
         alarm = options != null && options.isAlarmBroadcast();
         pushMessage = options != null && options.isPushMessagingBroadcast();
         pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
+        interactive = options != null && options.isInteractiveBroadcast();
         this.filterExtrasForReceiver = filterExtrasForReceiver;
     }
 
@@ -409,6 +413,7 @@
         callingPid = from.callingPid;
         callingUid = from.callingUid;
         callerInstantApp = from.callerInstantApp;
+        callerInstrumented = from.callerInstrumented;
         ordered = from.ordered;
         sticky = from.sticky;
         initialSticky = from.initialSticky;
@@ -450,6 +455,7 @@
         alarm = from.alarm;
         pushMessage = from.pushMessage;
         pushMessageOverQuota = from.pushMessageOverQuota;
+        interactive = from.interactive;
         filterExtrasForReceiver = from.filterExtrasForReceiver;
     }
 
@@ -611,6 +617,18 @@
         return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0;
     }
 
+    /**
+     * Core policy determination about this broadcast's delivery prioritization
+     */
+    boolean isUrgent() {
+        // TODO: flags for controlling policy
+        // TODO: migrate alarm-prioritization flag to BroadcastConstants
+        return (isForeground()
+                || interactive
+                || alarm)
+                && receivers.size() == 1;
+    }
+
     @NonNull String getHostingRecordTriggerType() {
         if (alarm) {
             return HostingRecord.TRIGGER_TYPE_ALARM;
@@ -656,6 +674,17 @@
         return (newIntent != null) ? newIntent : intent;
     }
 
+    static boolean isCallerInstrumented(@Nullable ProcessRecord callerApp, int callingUid) {
+        switch (UserHandle.getAppId(callingUid)) {
+            case android.os.Process.ROOT_UID:
+            case android.os.Process.SHELL_UID:
+                // Broadcasts sent via "shell" are typically invoked by test
+                // suites, so we treat them as if the caller was instrumented
+                return true;
+        }
+        return (callerApp != null) ? (callerApp.getActiveInstrumentation() != null) : false;
+    }
+
     /**
      * Return if given receivers list has more than one traunch of priorities.
      */
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 975619f..740efbc 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -443,13 +443,14 @@
         // invocation side effects such as allowlisting.
         if (options != null && callingUid != Process.SYSTEM_UID
                 && key.type == ActivityManager.INTENT_SENDER_BROADCAST) {
-            if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)) {
+            if (options.containsKey(BroadcastOptions.KEY_ALARM_BROADCAST)
+                    || options.containsKey(BroadcastOptions.KEY_INTERACTIVE_BROADCAST)) {
                 if (DEBUG_BROADCAST_LIGHT) {
                     Slog.w(TAG, "Non-system caller " + callingUid
-                            + " may not flag broadcast as alarm-related");
+                            + " may not flag broadcast as alarm or interactive");
                 }
                 throw new SecurityException(
-                        "Non-system callers may not flag broadcasts as alarm-related");
+                        "Non-system callers may not flag broadcasts as alarm or interactive");
             }
         }
 
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 3fa41c0..3c26116 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -3327,6 +3327,14 @@
             }
             EventLog.writeEvent(EventLogTags.UC_SEND_USER_BROADCAST, logUserId, intent.getAction());
 
+            // When the modern broadcast stack is enabled, deliver all our
+            // broadcasts as unordered, since the modern stack has better
+            // support for sequencing cold-starts, and it supports delivering
+            // resultTo for non-ordered broadcasts
+            if (mService.mEnableModernQueue) {
+                ordered = false;
+            }
+
             // TODO b/64165549 Verify that mLock is not held before calling AMS methods
             synchronized (mService) {
                 return mService.broadcastIntentLocked(null, null, null, intent, resolvedType,
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index e11d95a..efa2f25 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -490,6 +490,8 @@
         private final Object mModeConfigLock = new Object();
         @GuardedBy("mModeConfigLock")
         private final ArrayMap<Integer, GameModeConfiguration> mModeConfigs = new ArrayMap<>();
+        // if adding new properties or make any of the below overridable, the method
+        // copyAndApplyOverride should be updated accordingly
         private boolean mPerfModeOptedIn = false;
         private boolean mBatteryModeOptedIn = false;
         private boolean mAllowDownscale = true;
@@ -800,6 +802,42 @@
             }
         }
 
+        GamePackageConfiguration copyAndApplyOverride(GamePackageConfiguration overrideConfig) {
+            GamePackageConfiguration copy = new GamePackageConfiguration(mPackageName);
+            // if a game mode is overridden, we treat it with the highest priority and reset any
+            // opt-in game modes so that interventions are always executed.
+            copy.mPerfModeOptedIn = mPerfModeOptedIn && !(overrideConfig != null
+                    && overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE)
+                    != null);
+            copy.mBatteryModeOptedIn = mBatteryModeOptedIn && !(overrideConfig != null
+                    && overrideConfig.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY)
+                    != null);
+
+            // if any game mode is overridden, we will consider all interventions forced-active,
+            // this can be done more granular by checking if a specific intervention is
+            // overridden under each game mode override, but only if necessary.
+            copy.mAllowDownscale = mAllowDownscale || overrideConfig != null;
+            copy.mAllowAngle = mAllowAngle || overrideConfig != null;
+            copy.mAllowFpsOverride = mAllowFpsOverride || overrideConfig != null;
+            if (overrideConfig != null) {
+                synchronized (copy.mModeConfigLock) {
+                    synchronized (mModeConfigLock) {
+                        for (Map.Entry<Integer, GameModeConfiguration> entry :
+                                mModeConfigs.entrySet()) {
+                            copy.mModeConfigs.put(entry.getKey(), entry.getValue());
+                        }
+                    }
+                    synchronized (overrideConfig.mModeConfigLock) {
+                        for (Map.Entry<Integer, GameModeConfiguration> entry :
+                                overrideConfig.mModeConfigs.entrySet()) {
+                            copy.mModeConfigs.put(entry.getKey(), entry.getValue());
+                        }
+                    }
+                }
+            }
+            return copy;
+        }
+
         public String toString() {
             synchronized (mModeConfigLock) {
                 return "[Name:" + mPackageName + " Modes: " + mModeConfigs.toString() + "]";
@@ -1298,7 +1336,7 @@
         try {
             final float fps = 0.0f;
             final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
-            nativeSetOverrideFrameRate(uid, fps);
+            setOverrideFrameRate(uid, fps);
         } catch (PackageManager.NameNotFoundException e) {
             return;
         }
@@ -1330,7 +1368,7 @@
         try {
             final float fps = modeConfig.getFps();
             final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
-            nativeSetOverrideFrameRate(uid, fps);
+            setOverrideFrameRate(uid, fps);
         } catch (PackageManager.NameNotFoundException e) {
             return;
         }
@@ -1339,18 +1377,18 @@
 
     private void updateInterventions(String packageName,
             @GameMode int gameMode, @UserIdInt int userId) {
-        if (gameMode == GameManager.GAME_MODE_STANDARD
-                || gameMode == GameManager.GAME_MODE_UNSUPPORTED) {
-            resetFps(packageName, userId);
-            return;
-        }
         final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
-        if (packageConfig == null) {
-            Slog.v(TAG, "Package configuration not found for " + packageName);
-            return;
-        }
-        if (packageConfig.willGamePerformOptimizations(gameMode)) {
-            return;
+        if (gameMode == GameManager.GAME_MODE_STANDARD
+                || gameMode == GameManager.GAME_MODE_UNSUPPORTED || packageConfig == null
+                || packageConfig.willGamePerformOptimizations(gameMode)) {
+            resetFps(packageName, userId);
+            // resolution scaling does not need to be reset as it's now read dynamically on game
+            // restart, see #getResolutionScalingFactor and CompatModePackages#getCompatScale.
+            // TODO: reset Angle intervention here once implemented
+            if (packageConfig == null) {
+                Slog.v(TAG, "Package configuration not found for " + packageName);
+                return;
+            }
         }
         updateFps(packageConfig, packageName, gameMode, userId);
         updateUseAngle(packageName, gameMode);
@@ -1375,7 +1413,7 @@
             // look for the existing GamePackageConfiguration override
             configOverride = settings.getConfigOverride(packageName);
             if (configOverride == null) {
-                configOverride = new GamePackageConfiguration(mPackageManager, packageName, userId);
+                configOverride = new GamePackageConfiguration(packageName);
                 settings.setConfigOverride(packageName, configOverride);
             }
         }
@@ -1430,18 +1468,12 @@
                     return;
                 }
                 // if the game mode to reset is the only mode other than standard mode or there
-                // is device config, the config override is removed.
+                // is device config, the entire package config override is removed.
                 if (Integer.bitCount(modesBitfield) <= 2 || deviceConfig == null) {
                     settings.removeConfigOverride(packageName);
                 } else {
-                    final GamePackageConfiguration.GameModeConfiguration defaultModeConfig =
-                            deviceConfig.getGameModeConfiguration(gameModeToReset);
-                    // otherwise we reset the mode by copying the original config.
-                    if (defaultModeConfig == null) {
-                        configOverride.removeModeConfig(gameModeToReset);
-                    } else {
-                        configOverride.addModeConfig(defaultModeConfig);
-                    }
+                    // otherwise we reset the mode by removing the game mode config override
+                    configOverride.removeModeConfig(gameModeToReset);
                 }
             } else {
                 settings.removeConfigOverride(packageName);
@@ -1661,18 +1693,21 @@
      * @hide
      */
     public GamePackageConfiguration getConfig(String packageName, int userId) {
-        GamePackageConfiguration packageConfig = null;
+        GamePackageConfiguration overrideConfig = null;
+        GamePackageConfiguration config;
+        synchronized (mDeviceConfigLock) {
+            config = mConfigs.get(packageName);
+        }
+
         synchronized (mLock) {
             if (mSettings.containsKey(userId)) {
-                packageConfig = mSettings.get(userId).getConfigOverride(packageName);
+                overrideConfig = mSettings.get(userId).getConfigOverride(packageName);
             }
         }
-        if (packageConfig == null) {
-            synchronized (mDeviceConfigLock) {
-                packageConfig = mConfigs.get(packageName);
-            }
+        if (overrideConfig == null || config == null) {
+            return overrideConfig == null ? config : overrideConfig;
         }
-        return packageConfig;
+        return config.copyAndApplyOverride(overrideConfig);
     }
 
     private void registerPackageReceiver() {
@@ -1774,6 +1809,11 @@
         return handlerThread;
     }
 
+    @VisibleForTesting
+    void setOverrideFrameRate(int uid, float frameRate) {
+        nativeSetOverrideFrameRate(uid, frameRate);
+    }
+
     /**
      * load dynamic library for frame rate overriding JNI calls
      */
diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java
index 1162498..1e68837 100644
--- a/services/core/java/com/android/server/app/GameManagerSettings.java
+++ b/services/core/java/com/android/server/app/GameManagerSettings.java
@@ -21,12 +21,12 @@
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.app.GameManagerService.GamePackageConfiguration;
 import com.android.server.app.GameManagerService.GamePackageConfiguration.GameModeConfiguration;
 
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index bc650ad..a58583c 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -131,8 +131,6 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -152,6 +150,8 @@
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.LockGuard;
 import com.android.server.SystemServerInitThreadPool;
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index dd0c4b86..10243e2 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -53,13 +53,13 @@
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
index 2c68aaf..bd9d057 100644
--- a/services/core/java/com/android/server/appop/HistoricalRegistry.java
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -52,8 +52,6 @@
 import android.util.LongSparseArray;
 import android.util.Slog;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -62,6 +60,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.FgThread;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index cbfd17f0..f3a9a69 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4024,7 +4024,7 @@
         }
     }
 
-    private void setLeAudioVolumeOnModeUpdate(int mode, int streamType, int device) {
+    private void setLeAudioVolumeOnModeUpdate(int mode, int device) {
         switch (mode) {
             case AudioSystem.MODE_IN_COMMUNICATION:
             case AudioSystem.MODE_IN_CALL:
@@ -4038,10 +4038,16 @@
                 return;
         }
 
-        // Currently, DEVICE_OUT_BLE_HEADSET is the only output type for LE_AUDIO profile.
-        // (See AudioDeviceBroker#createBtDeviceInfo())
-        int index = mStreamStates[streamType].getIndex(AudioSystem.DEVICE_OUT_BLE_HEADSET);
-        int maxIndex = mStreamStates[streamType].getMaxIndex();
+        // Forcefully set LE audio volume as a workaround, since in some cases
+        // (like the outgoing call) the value of 'device' is not DEVICE_OUT_BLE_*
+        // even when BLE is connected.
+        if (!AudioSystem.isLeAudioDeviceType(device)) {
+            device = AudioSystem.DEVICE_OUT_BLE_HEADSET;
+        }
+
+        final int streamType = getBluetoothContextualVolumeStream(mode);
+        final int index = mStreamStates[streamType].getIndex(device);
+        final int maxIndex = mStreamStates[streamType].getMaxIndex();
 
         if (DEBUG_VOL) {
             Log.d(TAG, "setLeAudioVolumeOnModeUpdate postSetLeAudioVolumeIndex index="
@@ -5427,9 +5433,7 @@
                 // change of mode may require volume to be re-applied on some devices
                 updateAbsVolumeMultiModeDevices(previousMode, mode);
 
-                // Forcefully set LE audio volume as a workaround, since the value of 'device'
-                // is not DEVICE_OUT_BLE_* even when BLE is connected.
-                setLeAudioVolumeOnModeUpdate(mode, streamType, device);
+                setLeAudioVolumeOnModeUpdate(mode, device);
 
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
index 1370fd8..da7781a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.hardware.biometrics.BiometricConstants;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -293,4 +294,30 @@
                 + ", requestId=" + getRequestId()
                 + ", userId=" + getTargetUserId() + "}";
     }
+
+    /**
+     * Cancels this ClientMonitor
+     */
+    public void cancel() {
+        cancelWithoutStarting(mCallback);
+    }
+
+    /**
+     * Cancels this ClientMonitor without starting
+     * @param callback
+     */
+    public void cancelWithoutStarting(@NonNull ClientMonitorCallback callback) {
+        Slog.d(TAG, "cancelWithoutStarting: " + this);
+
+        final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED;
+        try {
+            ClientMonitorCallbackConverter listener = getListener();
+            if (listener != null) {
+                listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */);
+            }
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to invoke sendError", e);
+        }
+        callback.onClientFinished(this, true /* success */);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index 9317c4e..fb978b2 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -543,4 +543,37 @@
         mPendingOperations.clear();
         mCurrentOperation = null;
     }
+
+    /**
+     * Marks all pending operations as canceling and cancels the current
+     * operation.
+     */
+    private void clearScheduler() {
+        if (mCurrentOperation == null) {
+            return;
+        }
+        for (BiometricSchedulerOperation pendingOperation : mPendingOperations) {
+            Slog.d(getTag(), "[Watchdog cancelling pending] "
+                    + pendingOperation.getClientMonitor());
+            pendingOperation.markCanceling();
+        }
+        Slog.d(getTag(), "[Watchdog cancelling current] "
+                + mCurrentOperation.getClientMonitor());
+        mCurrentOperation.cancel(mHandler, getInternalCallback());
+    }
+
+    /**
+     * Start the timeout for the watchdog.
+     */
+    public void startWatchdog() {
+        if (mCurrentOperation == null) {
+            return;
+        }
+        final BiometricSchedulerOperation mOperation = mCurrentOperation;
+        mHandler.postDelayed(() -> {
+            if (mOperation == mCurrentOperation) {
+                clearScheduler();
+            }
+        }, 10000);
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index ef2931f..dacec38 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -267,7 +267,7 @@
 
     /** Flags this operation as canceled, if possible, but does not cancel it until started. */
     public boolean markCanceling() {
-        if (mState == STATE_WAITING_IN_QUEUE && isInterruptable()) {
+        if (mState == STATE_WAITING_IN_QUEUE) {
             mState = STATE_WAITING_IN_QUEUE_CANCELING;
             return true;
         }
@@ -287,10 +287,6 @@
         }
 
         final int currentState = mState;
-        if (!isInterruptable()) {
-            Slog.w(TAG, "Cannot cancel - operation not interruptable: " + this);
-            return;
-        }
         if (currentState == STATE_STARTED_CANCELING) {
             Slog.w(TAG, "Cannot cancel - already invoked for operation: " + this);
             return;
@@ -301,10 +297,10 @@
                 || currentState == STATE_WAITING_IN_QUEUE_CANCELING
                 || currentState == STATE_WAITING_FOR_COOKIE) {
             Slog.d(TAG, "[Cancelling] Current client (without start): " + mClientMonitor);
-            ((Interruptable) mClientMonitor).cancelWithoutStarting(getWrappedCallback(callback));
+            mClientMonitor.cancelWithoutStarting(getWrappedCallback(callback));
         } else {
             Slog.d(TAG, "[Cancelling] Current client: " + mClientMonitor);
-            ((Interruptable) mClientMonitor).cancel();
+            mClientMonitor.cancel();
         }
 
         // forcibly finish this client if the HAL does not acknowledge within the timeout
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
index 49cddaa..7fb27b6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricUserState.java
@@ -23,11 +23,11 @@
 import android.os.Environment;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 271bce9..2761ec0 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -183,6 +183,18 @@
                     receiver, opPackageName, disabledFeatures, previewSurface, debugConsent);
         }
 
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void scheduleWatchdog() {
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider == null) {
+                Slog.w(TAG, "Null provider for scheduling watchdog");
+                return;
+            }
+
+            provider.second.scheduleWatchdog(provider.first);
+        }
+
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public long enrollRemotely(int userId, final IBinder token, final byte[] hardwareAuthToken,
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java
index a9981d0..5a82b3a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceUserState.java
@@ -19,10 +19,10 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.face.Face;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.biometrics.sensors.BiometricUserState;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
index 4efaedb..85f95ce 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
@@ -128,4 +128,10 @@
             @NonNull String opPackageName);
 
     void dumpHal(int sensorId, @NonNull FileDescriptor fd, @NonNull String[] args);
+
+    /**
+     * Schedules watchdog for canceling hung operations
+     * @param sensorId sensor ID of the associated operation
+     */
+    default void scheduleWatchdog(int sensorId) {}
 }
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 b60f9d8..c12994c 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.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.InvalidationRequesterClient;
@@ -661,4 +662,14 @@
     void setTestHalEnabled(boolean enabled) {
         mTestHalEnabled = enabled;
     }
+
+    @Override
+    public void scheduleWatchdog(int sensorId) {
+        Slog.d(getTag(), "Starting watchdog for face");
+        final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
+        if (biometricScheduler == null) {
+            return;
+        }
+        biometricScheduler.startWatchdog();
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 7e2742e..b0dc28d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -879,6 +879,18 @@
                 provider.onPowerPressed();
             }
         }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void scheduleWatchdog() {
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
+            if (provider == null) {
+                Slog.w(TAG, "Null provider for scheduling watchdog");
+                return;
+            }
+
+            provider.second.scheduleWatchdog(provider.first);
+        }
     };
 
     public FingerprintService(Context context) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java
index ae173f7..b1a9ef1 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintUserState.java
@@ -19,10 +19,10 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.fingerprint.Fingerprint;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.biometrics.sensors.BiometricUserState;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
index 9075e7e..0c29f56 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
@@ -140,4 +140,10 @@
     @NonNull
     ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName);
+
+    /**
+     * Schedules watchdog for canceling hung operations
+     * @param sensorId sensor ID of the associated operation
+     */
+    default void scheduleWatchdog(int sensorId) {}
 }
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 650894d..17ba07f 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
@@ -59,6 +59,7 @@
 import com.android.server.biometrics.log.BiometricLogger;
 import com.android.server.biometrics.sensors.AuthenticationClient;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
+import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -779,4 +780,14 @@
         }
         return null;
     }
+
+    @Override
+    public void scheduleWatchdog(int sensorId) {
+        Slog.d(getTag(), "Starting watchdog for fingerprint");
+        final BiometricScheduler biometricScheduler = mSensors.get(sensorId).getScheduler();
+        if (biometricScheduler == null) {
+            return;
+        }
+        biometricScheduler.startWatchdog();
+    }
 }
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 0770062..6a01042 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -29,6 +29,8 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl;
 import com.android.server.utils.Slogf;
 
 import java.io.FileDescriptor;
@@ -47,7 +49,7 @@
     private static final List<String> SERVICE_NAMES = Arrays.asList(
             IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
 
-    private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl;
+    private final BroadcastRadioServiceImpl mHalAidl;
     private final BroadcastRadioService mService;
 
     /**
@@ -65,10 +67,15 @@
     }
 
     IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) {
+        this(service, new BroadcastRadioServiceImpl(serviceList));
         Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service);
-        mService = Objects.requireNonNull(service);
-        mHalAidl =
-                new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList);
+    }
+
+    @VisibleForTesting
+    IRadioServiceAidlImpl(BroadcastRadioService service, BroadcastRadioServiceImpl halAidl) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHalAidl = Objects.requireNonNull(halAidl,
+                "Broadcast radio service implementation for AIDL HAL cannot be null");
     }
 
     @Override
@@ -96,8 +103,8 @@
         if (isDebugEnabled()) {
             Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         return mHalAidl.addAnnouncementListener(enabledTypes, listener);
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 28b6d02..a8e4034 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.broadcastradio.hal2.AnnouncementAggregator;
 
 import java.io.FileDescriptor;
@@ -53,7 +54,7 @@
     private final List<RadioManager.ModuleProperties> mV1Modules;
 
     IRadioServiceHidlImpl(BroadcastRadioService service) {
-        mService = Objects.requireNonNull(service);
+        mService = Objects.requireNonNull(service, "broadcast radio service cannot be null");
         mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
         mV1Modules = mHal1.loadModules();
         OptionalInt max = mV1Modules.stream().mapToInt(RadioManager.ModuleProperties::getId).max();
@@ -61,6 +62,18 @@
                 max.isPresent() ? max.getAsInt() + 1 : 0, mLock);
     }
 
+    @VisibleForTesting
+    IRadioServiceHidlImpl(BroadcastRadioService service,
+            com.android.server.broadcastradio.hal1.BroadcastRadioService hal1,
+            com.android.server.broadcastradio.hal2.BroadcastRadioService hal2) {
+        mService = Objects.requireNonNull(service, "Broadcast radio service cannot be null");
+        mHal1 = Objects.requireNonNull(hal1,
+                "Broadcast radio service implementation for HIDL 1 HAL cannot be null");
+        mV1Modules = mHal1.loadModules();
+        mHal2 = Objects.requireNonNull(hal2,
+                "Broadcast radio service implementation for HIDL 2 HAL cannot be null");
+    }
+
     @Override
     public List<RadioManager.ModuleProperties> listModules() {
         mService.enforcePolicyAccess();
@@ -95,8 +108,8 @@
         if (isDebugEnabled()) {
             Slog.d(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes));
         }
-        Objects.requireNonNull(enabledTypes);
-        Objects.requireNonNull(listener);
+        Objects.requireNonNull(enabledTypes, "Enabled announcement types cannot be null");
+        Objects.requireNonNull(listener, "Announcement listener cannot be null");
         mService.enforcePolicyAccess();
 
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index 5c679b8..9c1cf38 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -51,8 +51,6 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoInputStream;
 import android.util.proto.ProtoOutputStream;
@@ -60,6 +58,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IntPair;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
index 7c9a484..3581981 100644
--- a/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
+++ b/services/core/java/com/android/server/display/AmbientBrightnessStatsTracker.java
@@ -22,12 +22,12 @@
 import android.os.SystemClock;
 import android.os.UserManager;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/display/BrightnessTracker.java b/services/core/java/com/android/server/display/BrightnessTracker.java
index 1686cb2..e9856d0 100644
--- a/services/core/java/com/android/server/display/BrightnessTracker.java
+++ b/services/core/java/com/android/server/display/BrightnessTracker.java
@@ -55,8 +55,6 @@
 import android.provider.Settings;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -64,6 +62,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.RingBuffer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import libcore.io.IoUtils;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 587db41..7e80b7d 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -40,6 +40,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.compat.CompatChanges;
@@ -101,6 +102,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
@@ -117,6 +119,7 @@
 import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.SurfaceControl;
+import android.view.SurfaceControl.RefreshRateRange;
 import android.window.DisplayWindowPolicyController;
 import android.window.ScreenCapture;
 
@@ -202,8 +205,6 @@
     private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable";
 
     private static final String PROP_DEFAULT_DISPLAY_TOP_INSET = "persist.sys.displayinset.top";
-    private static final String PROP_USE_NEW_DISPLAY_POWER_CONTROLLER =
-            "persist.sys.use_new_display_power_controller";
     private static final long WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT = 10000;
     // This value needs to be in sync with the threshold
     // in RefreshRateConfigs::getFrameRateDivisor.
@@ -1356,11 +1357,19 @@
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
-                final int displayId = createVirtualDisplayLocked(callback, projection, callingUid,
-                        packageName, surface, flags, virtualDisplayConfig);
+                final int displayId =
+                        createVirtualDisplayLocked(
+                                callback,
+                                projection,
+                                callingUid,
+                                packageName,
+                                virtualDevice,
+                                surface,
+                                flags,
+                                virtualDisplayConfig);
                 if (displayId != Display.INVALID_DISPLAY && virtualDevice != null && dwpc != null) {
-                    mDisplayWindowPolicyControllers.put(displayId,
-                            Pair.create(virtualDevice, dwpc));
+                    mDisplayWindowPolicyControllers.put(
+                            displayId, Pair.create(virtualDevice, dwpc));
                 }
                 return displayId;
             }
@@ -1369,12 +1378,20 @@
         }
     }
 
-    private int createVirtualDisplayLocked(IVirtualDisplayCallback callback,
-            IMediaProjection projection, int callingUid, String packageName, Surface surface,
-            int flags, VirtualDisplayConfig virtualDisplayConfig) {
+    private int createVirtualDisplayLocked(
+            IVirtualDisplayCallback callback,
+            IMediaProjection projection,
+            int callingUid,
+            String packageName,
+            IVirtualDevice virtualDevice,
+            Surface surface,
+            int flags,
+            VirtualDisplayConfig virtualDisplayConfig) {
         if (mVirtualDisplayAdapter == null) {
-            Slog.w(TAG, "Rejecting request to create private virtual display "
-                    + "because the virtual display adapter is not available.");
+            Slog.w(
+                    TAG,
+                    "Rejecting request to create private virtual display "
+                            + "because the virtual display adapter is not available.");
             return -1;
         }
 
@@ -1385,6 +1402,19 @@
             return -1;
         }
 
+        // If the display is to be added to a device display group, we need to make the
+        // LogicalDisplayMapper aware of the link between the new display and its associated virtual
+        // device before triggering DISPLAY_DEVICE_EVENT_ADDED.
+        if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0) {
+            try {
+                final int virtualDeviceId = virtualDevice.getDeviceId();
+                mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(
+                        device, virtualDeviceId);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+
         // DisplayDevice events are handled manually for Virtual Displays.
         // TODO: multi-display Fix this so that generic add/remove events are not handled in a
         // different code path for virtual displays.  Currently this happens so that we can
@@ -1393,8 +1423,7 @@
         // called on the DisplayThread (which we don't want to wait for?).
         // One option would be to actually wait here on the binder thread
         // to be notified when the virtual display is created (or failed).
-        mDisplayDeviceRepo.onDisplayDeviceEvent(device,
-                DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        mDisplayDeviceRepo.onDisplayDeviceEvent(device, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
 
         final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(device);
         if (display != null) {
@@ -2575,6 +2604,7 @@
         mLogicalDisplayMapper.forEachLocked(this::addDisplayPowerControllerLocked);
     }
 
+    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     private void addDisplayPowerControllerLocked(LogicalDisplay display) {
         if (mPowerHandler == null) {
             // initPowerManagement has not yet been called.
@@ -2588,7 +2618,8 @@
                 display, mSyncRoot);
         final DisplayPowerControllerInterface displayPowerController;
 
-        if (SystemProperties.getInt(PROP_USE_NEW_DISPLAY_POWER_CONTROLLER, 0) == 1) {
+        if (DeviceConfig.getBoolean("display_manager",
+                "use_newly_structured_display_power_controller", false)) {
             displayPowerController = new DisplayPowerController2(
                     mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
                     mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 912b1b2..aa9f2dc 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -33,7 +33,6 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.hardware.fingerprint.IUdfpsHbmListener;
 import android.net.Uri;
 import android.os.Handler;
@@ -49,6 +48,7 @@
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfigInterface;
 import android.provider.Settings;
+import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
@@ -58,6 +58,8 @@
 import android.util.SparseIntArray;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.view.SurfaceControl.RefreshRateRange;
+import android.view.SurfaceControl.RefreshRateRanges;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -135,6 +137,9 @@
 
     private boolean mAlwaysRespectAppRequest;
 
+    // TODO(b/241447632): remove the flag once SF changes are ready
+    private final boolean mRenderFrameRateIsPhysicalRefreshRate;
+
     /**
      * The allowed refresh rate switching type. This is used by SurfaceFlinger.
      */
@@ -170,6 +175,7 @@
         mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(),
                 mDeviceConfigDisplaySettings);
         mAlwaysRespectAppRequest = false;
+        mRenderFrameRateIsPhysicalRefreshRate = injector.renderFrameRateIsPhysicalRefreshRate();
     }
 
     /**
@@ -230,28 +236,48 @@
                 }
             }
         }
+
+        if (mRenderFrameRateIsPhysicalRefreshRate) {
+            for (int i = 0; i < votes.size(); i++) {
+
+                Vote vote = votes.valueAt(i);
+                vote.refreshRateRanges.physical.min = Math.max(vote.refreshRateRanges.physical.min,
+                        vote.refreshRateRanges.render.min);
+                vote.refreshRateRanges.physical.max = Math.min(vote.refreshRateRanges.physical.max,
+                        vote.refreshRateRanges.render.max);
+                vote.refreshRateRanges.render.min = Math.max(vote.refreshRateRanges.physical.min,
+                        vote.refreshRateRanges.render.min);
+                vote.refreshRateRanges.render.max = Math.min(vote.refreshRateRanges.physical.max,
+                        vote.refreshRateRanges.render.max);
+            }
+        }
+
         return votes;
     }
 
     private static final class VoteSummary {
-        public float minRefreshRate;
-        public float maxRefreshRate;
+        public float minPhysicalRefreshRate;
+        public float maxPhysicalRefreshRate;
+        public float minRenderFrameRate;
+        public float maxRenderFrameRate;
         public int width;
         public int height;
         public boolean disableRefreshRateSwitching;
-        public float baseModeRefreshRate;
+        public float appRequestBaseModeRefreshRate;
 
         VoteSummary() {
             reset();
         }
 
         public void reset() {
-            minRefreshRate = 0f;
-            maxRefreshRate = Float.POSITIVE_INFINITY;
+            minPhysicalRefreshRate = 0f;
+            maxPhysicalRefreshRate = Float.POSITIVE_INFINITY;
+            minRenderFrameRate = 0f;
+            maxRenderFrameRate = Float.POSITIVE_INFINITY;
             width = Vote.INVALID_SIZE;
             height = Vote.INVALID_SIZE;
             disableRefreshRateSwitching = false;
-            baseModeRefreshRate = 0f;
+            appRequestBaseModeRefreshRate = 0f;
         }
     }
 
@@ -270,9 +296,25 @@
             if (vote == null) {
                 continue;
             }
-            // For refresh rates, just use the tightest bounds of all the votes
-            summary.minRefreshRate = Math.max(summary.minRefreshRate, vote.refreshRateRange.min);
-            summary.maxRefreshRate = Math.min(summary.maxRefreshRate, vote.refreshRateRange.max);
+
+
+            // For physical refresh rates, just use the tightest bounds of all the votes.
+            // The refresh rate cannot be lower than the minimal render frame rate.
+            final float minPhysicalRefreshRate = Math.max(vote.refreshRateRanges.physical.min,
+                    vote.refreshRateRanges.render.min);
+            summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
+                    minPhysicalRefreshRate);
+            summary.maxPhysicalRefreshRate = Math.min(summary.maxPhysicalRefreshRate,
+                    vote.refreshRateRanges.physical.max);
+
+            // Same goes to render frame rate, but frame rate cannot exceed the max physical
+            // refresh rate
+            final float maxRenderFrameRate = Math.min(vote.refreshRateRanges.render.max,
+                    vote.refreshRateRanges.physical.max);
+            summary.minRenderFrameRate = Math.max(summary.minRenderFrameRate,
+                    vote.refreshRateRanges.render.min);
+            summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, maxRenderFrameRate);
+
             // For display size, disable refresh rate switching and base mode refresh rate use only
             // the first vote we come across (i.e. the highest priority vote that includes the
             // attribute).
@@ -284,12 +326,57 @@
             if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) {
                 summary.disableRefreshRateSwitching = true;
             }
-            if (summary.baseModeRefreshRate == 0f && vote.baseModeRefreshRate > 0f) {
-                summary.baseModeRefreshRate = vote.baseModeRefreshRate;
+            if (summary.appRequestBaseModeRefreshRate == 0f
+                    && vote.appRequestBaseModeRefreshRate > 0f) {
+                summary.appRequestBaseModeRefreshRate = vote.appRequestBaseModeRefreshRate;
+            }
+
+            if (mLoggingEnabled) {
+                Slog.w(TAG, "Vote summary for priority "
+                        + Vote.priorityToString(priority)
+                        + ": width=" + summary.width
+                        + ", height=" + summary.height
+                        + ", minPhysicalRefreshRate=" + summary.minPhysicalRefreshRate
+                        + ", maxPhysicalRefreshRate=" + summary.maxPhysicalRefreshRate
+                        + ", minRenderFrameRate=" + summary.minRenderFrameRate
+                        + ", maxRenderFrameRate=" + summary.maxRenderFrameRate
+                        + ", disableRefreshRateSwitching="
+                        + summary.disableRefreshRateSwitching
+                        + ", appRequestBaseModeRefreshRate="
+                        + summary.appRequestBaseModeRefreshRate);
             }
         }
     }
 
+    private boolean equalsWithinFloatTolerance(float a, float b) {
+        return a >= b - FLOAT_TOLERANCE && a <= b + FLOAT_TOLERANCE;
+    }
+
+    private Display.Mode selectBaseMode(VoteSummary summary,
+            ArrayList<Display.Mode> availableModes, Display.Mode defaultMode) {
+        // The base mode should be as close as possible to the app requested mode. Since all the
+        // available modes already have the same size, we just need to look for a matching refresh
+        // rate. If the summary doesn't include an app requested refresh rate, we'll use the default
+        // mode refresh rate. This is important because SurfaceFlinger can do only seamless switches
+        // by default. Some devices (e.g. TV) don't support seamless switching so the mode we select
+        // here won't be changed.
+        float preferredRefreshRate =
+                summary.appRequestBaseModeRefreshRate > 0
+                        ? summary.appRequestBaseModeRefreshRate : defaultMode.getRefreshRate();
+        for (Display.Mode availableMode : availableModes) {
+            if (equalsWithinFloatTolerance(preferredRefreshRate, availableMode.getRefreshRate())) {
+                return availableMode;
+            }
+        }
+
+        // If we couldn't find a mode id based on the refresh rate, it means that the available
+        // modes were filtered by the app requested size, which is different that the default mode
+        // size, and the requested app refresh rate was dropped from the summary due to a higher
+        // priority vote. Since we don't have any other hint about the refresh rate,
+        // we just pick the first.
+        return !availableModes.isEmpty() ? availableModes.get(0) : null;
+    }
+
     /**
      * Calculates the refresh rate ranges and display modes that the system is allowed to freely
      * switch between based on global and display-specific constraints.
@@ -346,11 +433,16 @@
                                 + " and constraints: "
                                 + "width=" + primarySummary.width
                                 + ", height=" + primarySummary.height
-                                + ", minRefreshRate=" + primarySummary.minRefreshRate
-                                + ", maxRefreshRate=" + primarySummary.maxRefreshRate
+                                + ", minPhysicalRefreshRate="
+                                + primarySummary.minPhysicalRefreshRate
+                                + ", maxPhysicalRefreshRate="
+                                + primarySummary.maxPhysicalRefreshRate
+                                + ", minRenderFrameRate=" + primarySummary.minRenderFrameRate
+                                + ", maxRenderFrameRate=" + primarySummary.maxRenderFrameRate
                                 + ", disableRefreshRateSwitching="
                                 + primarySummary.disableRefreshRateSwitching
-                                + ", baseModeRefreshRate=" + primarySummary.baseModeRefreshRate);
+                                + ", appRequestBaseModeRefreshRate="
+                                + primarySummary.appRequestBaseModeRefreshRate);
                     }
                     break;
                 }
@@ -361,11 +453,14 @@
                             + " and with the following constraints: "
                             + "width=" + primarySummary.width
                             + ", height=" + primarySummary.height
-                            + ", minRefreshRate=" + primarySummary.minRefreshRate
-                            + ", maxRefreshRate=" + primarySummary.maxRefreshRate
+                            + ", minPhysicalRefreshRate=" + primarySummary.minPhysicalRefreshRate
+                            + ", maxPhysicalRefreshRate=" + primarySummary.maxPhysicalRefreshRate
+                            + ", minRenderFrameRate=" + primarySummary.minRenderFrameRate
+                            + ", maxRenderFrameRate=" + primarySummary.maxRenderFrameRate
                             + ", disableRefreshRateSwitching="
                             + primarySummary.disableRefreshRateSwitching
-                            + ", baseModeRefreshRate=" + primarySummary.baseModeRefreshRate);
+                            + ", appRequestBaseModeRefreshRate="
+                            + primarySummary.appRequestBaseModeRefreshRate);
                 }
 
                 // If we haven't found anything with the current set of votes, drop the
@@ -373,72 +468,77 @@
                 lowestConsideredPriority++;
             }
 
+            if (mLoggingEnabled) {
+                Slog.i(TAG,
+                        "Primary physical range: ["
+                                + primarySummary.minPhysicalRefreshRate
+                                + " "
+                                + primarySummary.maxPhysicalRefreshRate
+                                + "] render frame rate range: ["
+                                + primarySummary.minRenderFrameRate
+                                + " "
+                                + primarySummary.maxRenderFrameRate
+                                + "]");
+            }
+
             VoteSummary appRequestSummary = new VoteSummary();
             summarizeVotes(
                     votes,
                     Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF,
                     Vote.MAX_PRIORITY,
                     appRequestSummary);
-            appRequestSummary.minRefreshRate =
-                    Math.min(appRequestSummary.minRefreshRate, primarySummary.minRefreshRate);
-            appRequestSummary.maxRefreshRate =
-                    Math.max(appRequestSummary.maxRefreshRate, primarySummary.maxRefreshRate);
+            appRequestSummary.minPhysicalRefreshRate =
+                    Math.min(appRequestSummary.minPhysicalRefreshRate,
+                            primarySummary.minPhysicalRefreshRate);
+            appRequestSummary.maxPhysicalRefreshRate =
+                    Math.max(appRequestSummary.maxPhysicalRefreshRate,
+                            primarySummary.maxPhysicalRefreshRate);
+            appRequestSummary.minRenderFrameRate =
+                    Math.min(appRequestSummary.minRenderFrameRate,
+                            primarySummary.minRenderFrameRate);
+            appRequestSummary.maxRenderFrameRate =
+                    Math.max(appRequestSummary.maxRenderFrameRate,
+                            primarySummary.maxRenderFrameRate);
             if (mLoggingEnabled) {
                 Slog.i(TAG,
-                        String.format("App request range: [%.0f %.0f]",
-                                appRequestSummary.minRefreshRate,
-                                appRequestSummary.maxRefreshRate));
+                        "App request range: ["
+                                + appRequestSummary.minPhysicalRefreshRate
+                                + " "
+                                + appRequestSummary.maxPhysicalRefreshRate
+                                + "] Frame rate range: ["
+                                + appRequestSummary.minRenderFrameRate
+                                + " "
+                                + appRequestSummary.maxRenderFrameRate
+                                + "]");
             }
 
-            // Select the base mode id based on the base mode refresh rate, if available, since this
-            // will be the mode id the app voted for.
-            Display.Mode baseMode = null;
-            for (Display.Mode availableMode : availableModes) {
-                if (primarySummary.baseModeRefreshRate
-                        >= availableMode.getRefreshRate() - FLOAT_TOLERANCE
-                        && primarySummary.baseModeRefreshRate
-                        <= availableMode.getRefreshRate() + FLOAT_TOLERANCE) {
-                    baseMode = availableMode;
-                }
-            }
-
-            // Select the default mode if available. This is important because SurfaceFlinger
-            // can do only seamless switches by default. Some devices (e.g. TV) don't support
-            // seamless switching so the mode we select here won't be changed.
-            if (baseMode == null) {
-                for (Display.Mode availableMode : availableModes) {
-                    if (availableMode.getModeId() == defaultMode.getModeId()) {
-                        baseMode = defaultMode;
-                        break;
-                    }
-                }
-            }
-
-            // If the application requests a display mode by setting
-            // LayoutParams.preferredDisplayModeId, it will be the only available mode and it'll
-            // be stored as baseModeId.
-            if (baseMode == null && !availableModes.isEmpty()) {
-                baseMode = availableModes.get(0);
-            }
-
+            Display.Mode baseMode = selectBaseMode(primarySummary, availableModes, defaultMode);
             if (baseMode == null) {
                 Slog.w(TAG, "Can't find a set of allowed modes which satisfies the votes. Falling"
                         + " back to the default mode. Display = " + displayId + ", votes = " + votes
                         + ", supported modes = " + Arrays.toString(modes));
 
                 float fps = defaultMode.getRefreshRate();
+                final RefreshRateRange range = new RefreshRateRange(fps, fps);
+                final RefreshRateRanges ranges = new RefreshRateRanges(range, range);
                 return new DesiredDisplayModeSpecs(defaultMode.getModeId(),
                         /*allowGroupSwitching */ false,
-                        new RefreshRateRange(fps, fps),
-                        new RefreshRateRange(fps, fps));
+                        ranges, ranges);
             }
 
             if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE
                     || primarySummary.disableRefreshRateSwitching) {
                 float fps = baseMode.getRefreshRate();
-                primarySummary.minRefreshRate = primarySummary.maxRefreshRate = fps;
+                primarySummary.minPhysicalRefreshRate = primarySummary.maxPhysicalRefreshRate = fps;
+                if (mRenderFrameRateIsPhysicalRefreshRate) {
+                    primarySummary.minRenderFrameRate = primarySummary.maxRenderFrameRate = fps;
+                }
                 if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE) {
-                    appRequestSummary.minRefreshRate = appRequestSummary.maxRefreshRate = fps;
+                    primarySummary.minRenderFrameRate = primarySummary.maxRenderFrameRate = fps;
+                    appRequestSummary.minPhysicalRefreshRate =
+                            appRequestSummary.maxPhysicalRefreshRate = fps;
+                    appRequestSummary.minRenderFrameRate =
+                            appRequestSummary.maxRenderFrameRate = fps;
                 }
             }
 
@@ -447,17 +547,36 @@
 
             return new DesiredDisplayModeSpecs(baseMode.getModeId(),
                     allowGroupSwitching,
-                    new RefreshRateRange(
-                            primarySummary.minRefreshRate, primarySummary.maxRefreshRate),
-                    new RefreshRateRange(
-                            appRequestSummary.minRefreshRate, appRequestSummary.maxRefreshRate));
+                    new RefreshRateRanges(
+                            new RefreshRateRange(
+                                    primarySummary.minPhysicalRefreshRate,
+                                    primarySummary.maxPhysicalRefreshRate),
+                            new RefreshRateRange(
+                                primarySummary.minRenderFrameRate,
+                                primarySummary.maxRenderFrameRate)),
+                    new RefreshRateRanges(
+                            new RefreshRateRange(
+                                    appRequestSummary.minPhysicalRefreshRate,
+                                    appRequestSummary.maxPhysicalRefreshRate),
+                            new RefreshRateRange(
+                                    appRequestSummary.minRenderFrameRate,
+                                    appRequestSummary.maxRenderFrameRate)));
         }
     }
 
     private ArrayList<Display.Mode> filterModes(Display.Mode[] supportedModes,
             VoteSummary summary) {
+        if (summary.minRenderFrameRate > summary.maxRenderFrameRate + FLOAT_TOLERANCE) {
+            if (mLoggingEnabled) {
+                Slog.w(TAG, "Vote summary resulted in empty set (invalid frame rate range)"
+                        + ": minRenderFrameRate=" + summary.minRenderFrameRate
+                        + ", maxRenderFrameRate=" + summary.maxRenderFrameRate);
+            }
+            return new ArrayList<>();
+        }
+
         ArrayList<Display.Mode> availableModes = new ArrayList<>();
-        boolean missingBaseModeRefreshRate = summary.baseModeRefreshRate > 0f;
+        boolean missingBaseModeRefreshRate = summary.appRequestBaseModeRefreshRate > 0f;
         for (Display.Mode mode : supportedModes) {
             if (mode.getPhysicalWidth() != summary.width
                     || mode.getPhysicalHeight() != summary.height) {
@@ -470,24 +589,47 @@
                 }
                 continue;
             }
-            final float refreshRate = mode.getRefreshRate();
+            final float physicalRefreshRate = mode.getRefreshRate();
             // Some refresh rates are calculated based on frame timings, so they aren't *exactly*
             // equal to expected refresh rate. Given that, we apply a bit of tolerance to this
             // comparison.
-            if (refreshRate < (summary.minRefreshRate - FLOAT_TOLERANCE)
-                    || refreshRate > (summary.maxRefreshRate + FLOAT_TOLERANCE)) {
+            if (physicalRefreshRate < (summary.minPhysicalRefreshRate - FLOAT_TOLERANCE)
+                    || physicalRefreshRate > (summary.maxPhysicalRefreshRate + FLOAT_TOLERANCE)) {
                 if (mLoggingEnabled) {
                     Slog.w(TAG, "Discarding mode " + mode.getModeId()
                             + ", outside refresh rate bounds"
-                            + ": minRefreshRate=" + summary.minRefreshRate
-                            + ", maxRefreshRate=" + summary.maxRefreshRate
-                            + ", modeRefreshRate=" + refreshRate);
+                            + ": minPhysicalRefreshRate=" + summary.minPhysicalRefreshRate
+                            + ", maxPhysicalRefreshRate=" + summary.maxPhysicalRefreshRate
+                            + ", modeRefreshRate=" + physicalRefreshRate);
                 }
                 continue;
             }
+
+            // Check whether the render frame rate range is achievable by the mode's physical
+            // refresh rate, meaning that if a divisor of the physical refresh rate is in range
+            // of the render frame rate.
+            // For example for the render frame rate [50, 70]:
+            //   - 120Hz is in range as we can render at 60hz by skipping every other frame,
+            //     which is within the render rate range
+            //   - 90hz is not in range as none of the even divisors (i.e. 90, 45, 30)
+            //     fall within the acceptable render range.
+            final int divisor = (int) Math.ceil(physicalRefreshRate / summary.maxRenderFrameRate);
+            float adjustedPhysicalRefreshRate = physicalRefreshRate / divisor;
+            if (adjustedPhysicalRefreshRate < (summary.minRenderFrameRate - FLOAT_TOLERANCE)) {
+                if (mLoggingEnabled) {
+                    Slog.w(TAG, "Discarding mode " + mode.getModeId()
+                            + " with adjusted refresh rate: " + adjustedPhysicalRefreshRate
+                            + ", outside frame rate bounds"
+                            + ": minRenderFrameRate=" + summary.minRenderFrameRate
+                            + ", maxRenderFrameRate=" + summary.maxRenderFrameRate
+                            + ", modePhysicalRefreshRate=" + physicalRefreshRate);
+                }
+                continue;
+            }
+
             availableModes.add(mode);
-            if (mode.getRefreshRate() >= summary.baseModeRefreshRate - FLOAT_TOLERANCE
-                    && mode.getRefreshRate() <= summary.baseModeRefreshRate + FLOAT_TOLERANCE) {
+            if (equalsWithinFloatTolerance(mode.getRefreshRate(),
+                    summary.appRequestBaseModeRefreshRate)) {
                 missingBaseModeRefreshRate = false;
             }
         }
@@ -854,30 +996,30 @@
         public boolean allowGroupSwitching;
 
         /**
-         * The primary refresh rate range.
+         * The primary refresh rate ranges.
          */
-        public final RefreshRateRange primaryRefreshRateRange;
+        public final RefreshRateRanges primary;
         /**
-         * The app request refresh rate range. Lower priority considerations won't be included in
+         * The app request refresh rate ranges. Lower priority considerations won't be included in
          * this range, allowing SurfaceFlinger to consider additional refresh rates for apps that
          * call setFrameRate(). This range will be greater than or equal to the primary refresh rate
          * range, never smaller.
          */
-        public final RefreshRateRange appRequestRefreshRateRange;
+        public final RefreshRateRanges appRequest;
 
         public DesiredDisplayModeSpecs() {
-            primaryRefreshRateRange = new RefreshRateRange();
-            appRequestRefreshRateRange = new RefreshRateRange();
+            primary = new RefreshRateRanges();
+            appRequest = new RefreshRateRanges();
         }
 
         public DesiredDisplayModeSpecs(int baseModeId,
                 boolean allowGroupSwitching,
-                @NonNull RefreshRateRange primaryRefreshRateRange,
-                @NonNull RefreshRateRange appRequestRefreshRateRange) {
+                @NonNull RefreshRateRanges primary,
+                @NonNull RefreshRateRanges appRequest) {
             this.baseModeId = baseModeId;
             this.allowGroupSwitching = allowGroupSwitching;
-            this.primaryRefreshRateRange = primaryRefreshRateRange;
-            this.appRequestRefreshRateRange = appRequestRefreshRateRange;
+            this.primary = primary;
+            this.appRequest = appRequest;
         }
 
         /**
@@ -886,12 +1028,12 @@
         @Override
         public String toString() {
             return String.format("baseModeId=%d allowGroupSwitching=%b"
-                            + " primaryRefreshRateRange=[%.0f %.0f]"
-                            + " appRequestRefreshRateRange=[%.0f %.0f]",
-                    baseModeId, allowGroupSwitching, primaryRefreshRateRange.min,
-                    primaryRefreshRateRange.max, appRequestRefreshRateRange.min,
-                    appRequestRefreshRateRange.max);
+                            + " primary=%s"
+                            + " appRequest=%s",
+                    baseModeId, allowGroupSwitching, primary.toString(),
+                    appRequest.toString());
         }
+
         /**
          * Checks whether the two objects have the same values.
          */
@@ -913,11 +1055,11 @@
             if (allowGroupSwitching != desiredDisplayModeSpecs.allowGroupSwitching) {
                 return false;
             }
-            if (!primaryRefreshRateRange.equals(desiredDisplayModeSpecs.primaryRefreshRateRange)) {
+            if (!primary.equals(desiredDisplayModeSpecs.primary)) {
                 return false;
             }
-            if (!appRequestRefreshRateRange.equals(
-                        desiredDisplayModeSpecs.appRequestRefreshRateRange)) {
+            if (!appRequest.equals(
+                    desiredDisplayModeSpecs.appRequest)) {
                 return false;
             }
             return true;
@@ -925,8 +1067,7 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(baseModeId, allowGroupSwitching, primaryRefreshRateRange,
-                    appRequestRefreshRateRange);
+            return Objects.hash(baseModeId, allowGroupSwitching, primary, appRequest);
         }
 
         /**
@@ -935,18 +1076,24 @@
         public void copyFrom(DesiredDisplayModeSpecs other) {
             baseModeId = other.baseModeId;
             allowGroupSwitching = other.allowGroupSwitching;
-            primaryRefreshRateRange.min = other.primaryRefreshRateRange.min;
-            primaryRefreshRateRange.max = other.primaryRefreshRateRange.max;
-            appRequestRefreshRateRange.min = other.appRequestRefreshRateRange.min;
-            appRequestRefreshRateRange.max = other.appRequestRefreshRateRange.max;
+            primary.physical.min = other.primary.physical.min;
+            primary.physical.max = other.primary.physical.max;
+            primary.render.min = other.primary.render.min;
+            primary.render.max = other.primary.render.max;
+
+            appRequest.physical.min = other.appRequest.physical.min;
+            appRequest.physical.max = other.appRequest.physical.max;
+            appRequest.render.min = other.appRequest.render.min;
+            appRequest.render.max = other.appRequest.render.max;
         }
     }
 
     @VisibleForTesting
     static final class Vote {
-        // DEFAULT_FRAME_RATE votes for [0, DEFAULT]. As the lowest priority vote, it's overridden
-        // by all other considerations. It acts to set a default frame rate for a device.
-        public static final int PRIORITY_DEFAULT_REFRESH_RATE = 0;
+        // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
+        // priority vote, it's overridden by all other considerations. It acts to set a default
+        // frame rate for a device.
+        public static final int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
 
         // PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
         // null. It is used to set a preferred refresh rate value in case the higher priority votes
@@ -956,16 +1103,16 @@
         // High-brightness-mode may need a specific range of refresh-rates to function properly.
         public static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
 
-        // SETTING_MIN_REFRESH_RATE is used to propose a lower bound of display refresh rate.
+        // SETTING_MIN_RENDER_FRAME_RATE is used to propose a lower bound of the render frame rate.
         // It votes [MIN_REFRESH_RATE, Float.POSITIVE_INFINITY]
-        public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 3;
+        public static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
 
-        // APP_REQUEST_REFRESH_RATE_RANGE is used to for internal apps to limit the refresh
-        // rate in certain cases, mostly to preserve power.
+        // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
+        // frame rate in certain cases, mostly to preserve power.
         // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
         // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
         // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
-        public static final int PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE = 4;
+        public static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 4;
 
         // We split the app request into different priorities in case we can satisfy one desire
         // without the other.
@@ -973,21 +1120,33 @@
         // Application can specify preferred refresh rate with below attrs.
         // @see android.view.WindowManager.LayoutParams#preferredRefreshRate
         // @see android.view.WindowManager.LayoutParams#preferredDisplayModeId
-        // These translates into votes for the base mode refresh rate and resolution to be
-        // used by SurfaceFlinger as the policy of choosing the display mode. The system also
-        // forces some apps like denylisted app to run at a lower refresh rate.
+        //
+        // When the app specifies a LayoutParams#preferredDisplayModeId, in addition to the
+        // refresh rate, it also chooses a preferred size (resolution) as part of the selected
+        // mode id. The app preference is then translated to APP_REQUEST_BASE_MODE_REFRESH_RATE and
+        // optionally to APP_REQUEST_SIZE as well, if a mode id was selected.
+        // The system also forces some apps like denylisted app to run at a lower refresh rate.
         // @see android.R.array#config_highRefreshRateBlacklist
+        //
+        // When summarizing the votes and filtering the allowed display modes, these votes determine
+        // which mode id should be the base mode id to be sent to SurfaceFlinger:
+        // - APP_REQUEST_BASE_MODE_REFRESH_RATE is used to validate the vote summary. If a summary
+        //   includes a base mode refresh rate, but it is not in the refresh rate range, then the
+        //   summary is considered invalid so we could drop a lower priority vote and try again.
+        // - APP_REQUEST_SIZE is used to filter out display modes of a different size.
+        //
         // The preferred refresh rate is set on the main surface of the app outside of
         // DisplayModeDirector.
         // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
         public static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5;
         public static final int PRIORITY_APP_REQUEST_SIZE = 6;
 
-        // SETTING_PEAK_REFRESH_RATE has a high priority and will restrict the bounds of the rest
-        // of low priority voters. It votes [0, max(PEAK, MIN)]
-        public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 7;
+        // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
+        // rest of low priority voters. It votes [0, max(PEAK, MIN)]
+        public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
 
-        // LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on.
+        // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
+        // Settings.Global.LOW_POWER_MODE is on.
         public static final int PRIORITY_LOW_POWER_MODE = 8;
 
         // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
@@ -1010,13 +1169,13 @@
         // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
         // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
 
-        public static final int MIN_PRIORITY = PRIORITY_DEFAULT_REFRESH_RATE;
+        public static final int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
         public static final int MAX_PRIORITY = PRIORITY_UDFPS;
 
         // The cutoff for the app request refresh rate range. Votes with priorities lower than this
         // value will not be considered when constructing the app request refresh rate range.
         public static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
-                PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE;
+                PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE;
 
         /**
          * A value signifying an invalid width or height in a vote.
@@ -1032,9 +1191,9 @@
          */
         public final int height;
         /**
-         * Information about the min and max refresh rate DM would like to set the display to.
+         * Information about the refresh rate frame rate ranges DM would like to set the display to.
          */
-        public final RefreshRateRange refreshRateRange;
+        public final RefreshRateRanges refreshRateRanges;
 
         /**
          * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
@@ -1043,52 +1202,66 @@
         public final boolean disableRefreshRateSwitching;
 
         /**
-         * The base mode refresh rate to be used for this display. This would be used when deciding
-         * the base mode id.
+         * The preferred refresh rate selected by the app. It is used to validate that the summary
+         * refresh rate ranges include this value, and are not restricted by a lower priority vote.
          */
-        public final float baseModeRefreshRate;
+        public final float appRequestBaseModeRefreshRate;
 
-        public static Vote forRefreshRates(float minRefreshRate, float maxRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate,
+        public static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
+            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate, 0,
+                    Float.POSITIVE_INFINITY,
                     minRefreshRate == maxRefreshRate, 0f);
         }
 
+        public static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, minFrameRate,
+                    maxFrameRate,
+                    false, 0f);
+        }
+
         public static Vote forSize(int width, int height) {
-            return new Vote(width, height, 0f, Float.POSITIVE_INFINITY, false,
+            return new Vote(width, height, 0, Float.POSITIVE_INFINITY, 0, Float.POSITIVE_INFINITY,
+                    false,
                     0f);
         }
 
         public static Vote forDisableRefreshRateSwitching() {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0f, Float.POSITIVE_INFINITY, true,
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
+                    Float.POSITIVE_INFINITY, true,
                     0f);
         }
 
         public static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0f, Float.POSITIVE_INFINITY, false,
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
+                    Float.POSITIVE_INFINITY, false,
                     baseModeRefreshRate);
         }
 
         private Vote(int width, int height,
-                float minRefreshRate, float maxRefreshRate,
+                float minPhysicalRefreshRate,
+                float maxPhysicalRefreshRate,
+                float minRenderFrameRate,
+                float maxRenderFrameRate,
                 boolean disableRefreshRateSwitching,
                 float baseModeRefreshRate) {
             this.width = width;
             this.height = height;
-            this.refreshRateRange =
-                    new RefreshRateRange(minRefreshRate, maxRefreshRate);
+            this.refreshRateRanges = new RefreshRateRanges(
+                    new RefreshRateRange(minPhysicalRefreshRate, maxPhysicalRefreshRate),
+                    new RefreshRateRange(minRenderFrameRate, maxRenderFrameRate));
             this.disableRefreshRateSwitching = disableRefreshRateSwitching;
-            this.baseModeRefreshRate = baseModeRefreshRate;
+            this.appRequestBaseModeRefreshRate = baseModeRefreshRate;
         }
 
         public static String priorityToString(int priority) {
             switch (priority) {
                 case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
                     return "PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE";
-                case PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE:
-                    return "PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE";
+                case PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE:
+                    return "PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE";
                 case PRIORITY_APP_REQUEST_SIZE:
                     return "PRIORITY_APP_REQUEST_SIZE";
-                case PRIORITY_DEFAULT_REFRESH_RATE:
+                case PRIORITY_DEFAULT_RENDER_FRAME_RATE:
                     return "PRIORITY_DEFAULT_REFRESH_RATE";
                 case PRIORITY_FLICKER_REFRESH_RATE:
                     return "PRIORITY_FLICKER_REFRESH_RATE";
@@ -1104,10 +1277,10 @@
                     return "PRIORITY_SKIN_TEMPERATURE";
                 case PRIORITY_UDFPS:
                     return "PRIORITY_UDFPS";
-                case PRIORITY_USER_SETTING_MIN_REFRESH_RATE:
-                    return "PRIORITY_USER_SETTING_MIN_REFRESH_RATE";
-                case PRIORITY_USER_SETTING_PEAK_REFRESH_RATE:
-                    return "PRIORITY_USER_SETTING_PEAK_REFRESH_RATE";
+                case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE:
+                    return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
+                case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
+                    return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
                 default:
                     return Integer.toString(priority);
             }
@@ -1116,11 +1289,10 @@
         @Override
         public String toString() {
             return "Vote{"
-                + "width=" + width + ", height=" + height
-                + ", minRefreshRate=" + refreshRateRange.min
-                + ", maxRefreshRate=" + refreshRateRange.max
-                + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
-                + ", baseModeRefreshRate=" + baseModeRefreshRate + "}";
+                    + "width=" + width + ", height=" + height
+                    + ", refreshRateRanges=" + refreshRateRanges
+                    + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
+                    + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate + "}";
         }
     }
 
@@ -1237,7 +1409,7 @@
                     Settings.Global.LOW_POWER_MODE, 0 /*default*/) != 0;
             final Vote vote;
             if (inLowPowerMode) {
-                vote = Vote.forRefreshRates(0f, 60f);
+                vote = Vote.forRenderFrameRates(0f, 60f);
             } else {
                 vote = null;
             }
@@ -1262,13 +1434,14 @@
             // than necessary, and we should improve it. See b/156304339 for more info.
             Vote peakVote = peakRefreshRate == 0f
                     ? null
-                    : Vote.forRefreshRates(0f, Math.max(minRefreshRate, peakRefreshRate));
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, peakVote);
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                    Vote.forRefreshRates(minRefreshRate, Float.POSITIVE_INFINITY));
+                    : Vote.forRenderFrameRates(0f, Math.max(minRefreshRate, peakRefreshRate));
+            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE, peakVote);
+            updateVoteLocked(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                    Vote.forRenderFrameRates(minRefreshRate, Float.POSITIVE_INFINITY));
             Vote defaultVote =
-                    defaultRefreshRate == 0f ? null : Vote.forRefreshRates(0f, defaultRefreshRate);
-            updateVoteLocked(Vote.PRIORITY_DEFAULT_REFRESH_RATE, defaultVote);
+                    defaultRefreshRate == 0f
+                            ? null : Vote.forRenderFrameRates(0f, defaultRefreshRate);
+            updateVoteLocked(Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE, defaultVote);
 
             float maxRefreshRate;
             if (peakRefreshRate == 0f && defaultRefreshRate == 0f) {
@@ -1374,13 +1547,15 @@
 
             if (refreshRateRange != null) {
                 mAppPreferredRefreshRateRangeByDisplay.put(displayId, refreshRateRange);
-                vote = Vote.forRefreshRates(refreshRateRange.min, refreshRateRange.max);
+                vote = Vote.forRenderFrameRates(refreshRateRange.min, refreshRateRange.max);
             } else {
                 mAppPreferredRefreshRateRangeByDisplay.remove(displayId);
                 vote = null;
             }
             synchronized (mLock) {
-                updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, vote);
+                updateVoteLocked(displayId,
+                        Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                        vote);
             }
         }
 
@@ -1956,6 +2131,7 @@
 
             return false;
         }
+
         private void onBrightnessChangedLocked() {
             Vote refreshRateVote = null;
             Vote refreshRateSwitchingVote = null;
@@ -1969,7 +2145,7 @@
             boolean insideLowZone = hasValidLowZone() && isInsideLowZone(mBrightness, mAmbientLux);
             if (insideLowZone) {
                 refreshRateVote =
-                        Vote.forRefreshRates(mRefreshRateInLowZone, mRefreshRateInLowZone);
+                        Vote.forPhysicalRefreshRates(mRefreshRateInLowZone, mRefreshRateInLowZone);
                 refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
             }
 
@@ -1977,7 +2153,8 @@
                     && isInsideHighZone(mBrightness, mAmbientLux);
             if (insideHighZone) {
                 refreshRateVote =
-                        Vote.forRefreshRates(mRefreshRateInHighZone, mRefreshRateInHighZone);
+                        Vote.forPhysicalRefreshRates(mRefreshRateInHighZone,
+                                mRefreshRateInHighZone);
                 refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
             }
 
@@ -2222,7 +2399,7 @@
                         maxRefreshRate = mode.getRefreshRate();
                     }
                 }
-                vote = Vote.forRefreshRates(maxRefreshRate, maxRefreshRate);
+                vote = Vote.forPhysicalRefreshRates(maxRefreshRate, maxRefreshRate);
             } else {
                 vote = null;
             }
@@ -2303,7 +2480,7 @@
                             mDisplayManagerInternal.getRefreshRateForDisplayAndSensor(
                                     displayId, mProximitySensorName, mProximitySensorType);
                     if (rate != null) {
-                        vote = Vote.forRefreshRates(rate.min, rate.max);
+                        vote = Vote.forPhysicalRefreshRates(rate.min, rate.max);
                     }
                 }
                 mBallotBox.vote(displayId, Vote.PRIORITY_PROXIMITY, vote);
@@ -2472,7 +2649,7 @@
                 if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT) {
                     // Device resource properties take priority over DisplayDeviceConfig
                     if (mRefreshRateInHbmSunlight > 0) {
-                        vote = Vote.forRefreshRates(mRefreshRateInHbmSunlight,
+                        vote = Vote.forPhysicalRefreshRates(mRefreshRateInHbmSunlight,
                                 mRefreshRateInHbmSunlight);
                     } else {
                         final List<RefreshRateLimitation> limits =
@@ -2480,7 +2657,7 @@
                         for (int i = 0; limits != null && i < limits.size(); i++) {
                             final RefreshRateLimitation limitation = limits.get(i);
                             if (limitation.type == REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE) {
-                                vote = Vote.forRefreshRates(limitation.range.min,
+                                vote = Vote.forPhysicalRefreshRates(limitation.range.min,
                                         limitation.range.max);
                                 break;
                             }
@@ -2490,7 +2667,7 @@
                         mRefreshRateInHbmHdr > 0) {
                     // HBM for HDR vote isn't supported through DisplayDeviceConfig yet, so look for
                     // a vote from Device properties
-                    vote = Vote.forRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr);
+                    vote = Vote.forPhysicalRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr);
                 } else {
                     Slog.w(TAG, "Unexpected HBM mode " + hbmMode + " for display ID " + displayId);
                 }
@@ -2528,7 +2705,7 @@
             }
             final Vote vote;
             if (mStatus >= Temperature.THROTTLING_CRITICAL) {
-                vote = Vote.forRefreshRates(0f, 60f);
+                vote = Vote.forRenderFrameRates(0f, 60f);
             } else {
                 vote = null;
             }
@@ -2741,6 +2918,8 @@
         boolean isDozeState(Display d);
 
         IThermalService getThermalService();
+
+        boolean renderFrameRateIsPhysicalRefreshRate();
     }
 
     @VisibleForTesting
@@ -2794,6 +2973,12 @@
                     ServiceManager.getService(Context.THERMAL_SERVICE));
         }
 
+        @Override
+        public boolean renderFrameRateIsPhysicalRefreshRate() {
+            return DisplayProperties
+                    .debug_render_frame_rate_is_physical_refresh_rate().orElse(true);
+        }
+
         private DisplayManager getDisplayManager() {
             if (mDisplayManager == null) {
                 mDisplayManager = mContext.getSystemService(DisplayManager.class);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 002209e..2c2075d 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -387,14 +387,8 @@
                 // list of available modes will take care of updating display mode specs.
                 if (activeBaseMode == INVALID_MODE_ID
                         || mDisplayModeSpecs.baseModeId != activeBaseMode
-                        || mDisplayModeSpecs.primaryRefreshRateRange.min
-                                != modeSpecs.primaryRefreshRateMin
-                        || mDisplayModeSpecs.primaryRefreshRateRange.max
-                                != modeSpecs.primaryRefreshRateMax
-                        || mDisplayModeSpecs.appRequestRefreshRateRange.min
-                                != modeSpecs.appRequestRefreshRateMin
-                        || mDisplayModeSpecs.appRequestRefreshRateRange.max
-                                != modeSpecs.appRequestRefreshRateMax) {
+                        || !mDisplayModeSpecs.primary.equals(modeSpecs.primaryRanges)
+                        || !mDisplayModeSpecs.appRequest.equals(modeSpecs.appRequestRanges)) {
                     mDisplayModeSpecsInvalid = true;
                     sendTraversalRequestLocked();
                 }
@@ -997,10 +991,8 @@
                         getDisplayTokenLocked(),
                         new SurfaceControl.DesiredDisplayModeSpecs(baseSfModeId,
                                 mDisplayModeSpecs.allowGroupSwitching,
-                                mDisplayModeSpecs.primaryRefreshRateRange.min,
-                                mDisplayModeSpecs.primaryRefreshRateRange.max,
-                                mDisplayModeSpecs.appRequestRefreshRateRange.min,
-                                mDisplayModeSpecs.appRequestRefreshRateRange.max)));
+                                mDisplayModeSpecs.primary,
+                                mDisplayModeSpecs.appRequest)));
             }
         }
 
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 70c9e23..cb97e28 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -28,6 +28,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
@@ -123,6 +124,12 @@
     /** Map of all display groups indexed by display group id. */
     private final SparseArray<DisplayGroup> mDisplayGroups = new SparseArray<>();
 
+    /**
+     * Map of display groups which are linked to virtual devices (all displays in the group are
+     * linked to that device). Keyed by virtual device unique id.
+     */
+    private final SparseIntArray mDeviceDisplayGroupIds = new SparseIntArray();
+
     private final DisplayDeviceRepository mDisplayDeviceRepo;
     private final DeviceStateToLayoutMap mDeviceStateToLayoutMap;
     private final Listener mListener;
@@ -157,6 +164,12 @@
      */
     private final SparseIntArray mDisplayGroupsToUpdate = new SparseIntArray();
 
+    /**
+     * ArrayMap of display device unique ID to virtual device ID. Used in {@link
+     * #updateLogicalDisplaysLocked} to establish which Virtual Devices own which Virtual Displays.
+     */
+    private final ArrayMap<String, Integer> mVirtualDeviceDisplayMapping = new ArrayMap<>();
+
     private int mNextNonDefaultGroupId = Display.DEFAULT_DISPLAY_GROUP + 1;
     private Layout mCurrentLayout = null;
     private int mDeviceState = DeviceStateManager.INVALID_DEVICE_STATE;
@@ -362,6 +375,19 @@
         mDeviceStateToLayoutMap.dumpLocked(ipw);
     }
 
+    /**
+     * Creates an association between a displayDevice and a virtual device. Any displays associated
+     * with this virtual device will be grouped together in a single {@link DisplayGroup} unless
+     * created with {@link Display.FLAG_OWN_DISPLAY_GROUP}.
+     *
+     * @param displayDevice the displayDevice to be linked
+     * @param virtualDeviceUniqueId the unique ID of the virtual device.
+     */
+    void associateDisplayDeviceWithVirtualDevice(
+            DisplayDevice displayDevice, int virtualDeviceUniqueId) {
+        mVirtualDeviceDisplayMapping.put(displayDevice.getUniqueId(), virtualDeviceUniqueId);
+    }
+
     void setDeviceStateLocked(int state, boolean isOverrideActive) {
         Slog.i(TAG, "Requesting Transition to state: " + state + ", from state=" + mDeviceState
                 + ", interactive=" + mInteractive);
@@ -556,6 +582,9 @@
         }
         DisplayDeviceInfo deviceInfo = device.getDisplayDeviceInfoLocked();
 
+        // Remove any virtual device mapping which exists for the display.
+        mVirtualDeviceDisplayMapping.remove(device.getUniqueId());
+
         if (layoutDisplay.getAddress().equals(deviceInfo.address)) {
             layout.removeDisplayLocked(DEFAULT_DISPLAY);
 
@@ -749,24 +778,44 @@
                 // We wait until we sent the EVENT_REMOVED event before actually removing the
                 // group.
                 mDisplayGroups.delete(id);
+                // Remove possible reference to the removed group.
+                int deviceIndex = mDeviceDisplayGroupIds.indexOfValue(id);
+                if (deviceIndex >= 0) {
+                    mDeviceDisplayGroupIds.removeAt(deviceIndex);
+                }
             }
         }
     }
 
     private void assignDisplayGroupLocked(LogicalDisplay display) {
         final int displayId = display.getDisplayIdLocked();
+        final String primaryDisplayUniqueId = display.getPrimaryDisplayDeviceLocked().getUniqueId();
+        final Integer linkedDeviceUniqueId =
+                mVirtualDeviceDisplayMapping.get(primaryDisplayUniqueId);
 
         // Get current display group data
         int groupId = getDisplayGroupIdFromDisplayIdLocked(displayId);
+        Integer deviceDisplayGroupId = null;
+        if (linkedDeviceUniqueId != null
+                && mDeviceDisplayGroupIds.indexOfKey(linkedDeviceUniqueId) > 0) {
+            deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId);
+        }
         final DisplayGroup oldGroup = getDisplayGroupLocked(groupId);
 
         // Get the new display group if a change is needed
         final DisplayInfo info = display.getDisplayInfoLocked();
         final boolean needsOwnDisplayGroup = (info.flags & Display.FLAG_OWN_DISPLAY_GROUP) != 0;
         final boolean hasOwnDisplayGroup = groupId != Display.DEFAULT_DISPLAY_GROUP;
+        final boolean needsDeviceDisplayGroup =
+                !needsOwnDisplayGroup && linkedDeviceUniqueId != null;
+        final boolean hasDeviceDisplayGroup =
+                deviceDisplayGroupId != null && groupId == deviceDisplayGroupId;
         if (groupId == Display.INVALID_DISPLAY_GROUP
-                || hasOwnDisplayGroup != needsOwnDisplayGroup) {
-            groupId = assignDisplayGroupIdLocked(needsOwnDisplayGroup);
+                || hasOwnDisplayGroup != needsOwnDisplayGroup
+                || hasDeviceDisplayGroup != needsDeviceDisplayGroup) {
+            groupId =
+                    assignDisplayGroupIdLocked(
+                            needsOwnDisplayGroup, needsDeviceDisplayGroup, linkedDeviceUniqueId);
         }
 
         // Create a new group if needed
@@ -931,7 +980,17 @@
         display.setPhase(phase);
     }
 
-    private int assignDisplayGroupIdLocked(boolean isOwnDisplayGroup) {
+    private int assignDisplayGroupIdLocked(
+            boolean isOwnDisplayGroup, boolean isDeviceDisplayGroup, Integer linkedDeviceUniqueId) {
+        if (isDeviceDisplayGroup && linkedDeviceUniqueId != null) {
+            int deviceDisplayGroupId = mDeviceDisplayGroupIds.get(linkedDeviceUniqueId);
+            // A value of 0 indicates that no device display group was found.
+            if (deviceDisplayGroupId == 0) {
+                deviceDisplayGroupId = mNextNonDefaultGroupId++;
+                mDeviceDisplayGroupIds.put(linkedDeviceUniqueId, deviceDisplayGroupId);
+            }
+            return deviceDisplayGroupId;
+        }
         return isOwnDisplayGroup ? mNextNonDefaultGroupId++ : Display.DEFAULT_DISPLAY_GROUP;
     }
 
diff --git a/services/core/java/com/android/server/display/PersistentDataStore.java b/services/core/java/com/android/server/display/PersistentDataStore.java
index a11f172..f30a84f 100644
--- a/services/core/java/com/android/server/display/PersistentDataStore.java
+++ b/services/core/java/com/android/server/display/PersistentDataStore.java
@@ -26,14 +26,14 @@
 import android.util.SparseArray;
 import android.util.SparseLongArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index b8af1bf..cd9ef09 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -16,6 +16,9 @@
 
 package com.android.server.dreams;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
+
+import android.app.ActivityTaskManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -34,13 +37,13 @@
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamService;
 import android.util.Slog;
-import android.view.IWindowManager;
-import android.view.WindowManagerGlobal;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 /**
@@ -60,9 +63,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final Listener mListener;
-    private final IWindowManager mIWindowManager;
-    private long mDreamStartTime;
-    private String mSavedStopReason;
+    private final ActivityTaskManager mActivityTaskManager;
 
     private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
             .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
@@ -73,27 +74,21 @@
 
     private DreamRecord mCurrentDream;
 
-    private final Runnable mStopUnconnectedDreamRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mCurrentDream != null && mCurrentDream.mBound && !mCurrentDream.mConnected) {
-                Slog.w(TAG, "Bound dream did not connect in the time allotted");
-                stopDream(true /*immediate*/, "slow to connect");
-            }
-        }
-    };
+    // Whether a dreaming started intent has been broadcast.
+    private boolean mSentStartBroadcast = false;
 
-    private final Runnable mStopStubbornDreamRunnable = () -> {
-        Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
-        stopDream(true /*immediate*/, "slow to finish");
-        mSavedStopReason = null;
-    };
+    // When a new dream is started and there is an existing dream, the existing dream is allowed to
+    // live a little longer until the new dream is started, for a smoother transition. This dream is
+    // stopped as soon as the new dream is started, and this list is cleared. Usually there should
+    // only be one previous dream while waiting for a new dream to start, but we store a list to
+    // proof the edge case of multiple previous dreams.
+    private final ArrayList<DreamRecord> mPreviousDreams = new ArrayList<>();
 
     public DreamController(Context context, Handler handler, Listener listener) {
         mContext = context;
         mHandler = handler;
         mListener = listener;
-        mIWindowManager = WindowManagerGlobal.getWindowManagerService();
+        mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
         mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         mCloseNotificationShadeIntent.putExtra("reason", "dream");
     }
@@ -109,18 +104,17 @@
             pw.println("    mUserId=" + mCurrentDream.mUserId);
             pw.println("    mBound=" + mCurrentDream.mBound);
             pw.println("    mService=" + mCurrentDream.mService);
-            pw.println("    mSentStartBroadcast=" + mCurrentDream.mSentStartBroadcast);
             pw.println("    mWakingGently=" + mCurrentDream.mWakingGently);
         } else {
             pw.println("  mCurrentDream: null");
         }
+
+        pw.println("  mSentStartBroadcast=" + mSentStartBroadcast);
     }
 
     public void startDream(Binder token, ComponentName name,
             boolean isPreviewMode, boolean canDoze, int userId, PowerManager.WakeLock wakeLock,
             ComponentName overlayComponentName, String reason) {
-        stopDream(true /*immediate*/, "starting new dream");
-
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "startDream");
         try {
             // Close the notification shade. No need to send to all, but better to be explicit.
@@ -130,9 +124,12 @@
                     + ", isPreviewMode=" + isPreviewMode + ", canDoze=" + canDoze
                     + ", userId=" + userId + ", reason='" + reason + "'");
 
+            if (mCurrentDream != null) {
+                mPreviousDreams.add(mCurrentDream);
+            }
             mCurrentDream = new DreamRecord(token, name, isPreviewMode, canDoze, userId, wakeLock);
 
-            mDreamStartTime = SystemClock.elapsedRealtime();
+            mCurrentDream.mDreamStartTime = SystemClock.elapsedRealtime();
             MetricsLogger.visible(mContext,
                     mCurrentDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
 
@@ -155,31 +152,49 @@
             }
 
             mCurrentDream.mBound = true;
-            mHandler.postDelayed(mStopUnconnectedDreamRunnable, DREAM_CONNECTION_TIMEOUT);
+            mHandler.postDelayed(mCurrentDream.mStopUnconnectedDreamRunnable,
+                    DREAM_CONNECTION_TIMEOUT);
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
+    /**
+     * Stops dreaming.
+     *
+     * The current dream, if any, and any unstopped previous dreams are stopped. The device stops
+     * dreaming.
+     */
     public void stopDream(boolean immediate, String reason) {
-        if (mCurrentDream == null) {
+        stopPreviousDreams();
+        stopDreamInstance(immediate, reason, mCurrentDream);
+    }
+
+    /**
+     * Stops the given dream instance.
+     *
+     * The device may still be dreaming afterwards if there are other dreams running.
+     */
+    private void stopDreamInstance(boolean immediate, String reason, DreamRecord dream) {
+        if (dream == null) {
             return;
         }
 
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "stopDream");
         try {
             if (!immediate) {
-                if (mCurrentDream.mWakingGently) {
+                if (dream.mWakingGently) {
                     return; // already waking gently
                 }
 
-                if (mCurrentDream.mService != null) {
+                if (dream.mService != null) {
                     // Give the dream a moment to wake up and finish itself gently.
-                    mCurrentDream.mWakingGently = true;
+                    dream.mWakingGently = true;
                     try {
-                        mSavedStopReason = reason;
-                        mCurrentDream.mService.wakeUp();
-                        mHandler.postDelayed(mStopStubbornDreamRunnable, DREAM_FINISH_TIMEOUT);
+                        dream.mStopReason = reason;
+                        dream.mService.wakeUp();
+                        mHandler.postDelayed(dream.mStopStubbornDreamRunnable,
+                                DREAM_FINISH_TIMEOUT);
                         return;
                     } catch (RemoteException ex) {
                         // oh well, we tried, finish immediately instead
@@ -187,54 +202,76 @@
                 }
             }
 
-            final DreamRecord oldDream = mCurrentDream;
-            mCurrentDream = null;
-            Slog.i(TAG, "Stopping dream: name=" + oldDream.mName
-                    + ", isPreviewMode=" + oldDream.mIsPreviewMode
-                    + ", canDoze=" + oldDream.mCanDoze
-                    + ", userId=" + oldDream.mUserId
+            Slog.i(TAG, "Stopping dream: name=" + dream.mName
+                    + ", isPreviewMode=" + dream.mIsPreviewMode
+                    + ", canDoze=" + dream.mCanDoze
+                    + ", userId=" + dream.mUserId
                     + ", reason='" + reason + "'"
-                    + (mSavedStopReason == null ? "" : "(from '" + mSavedStopReason + "')"));
+                    + (dream.mStopReason == null ? "" : "(from '"
+                    + dream.mStopReason + "')"));
             MetricsLogger.hidden(mContext,
-                    oldDream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
+                    dream.mCanDoze ? MetricsEvent.DOZING : MetricsEvent.DREAMING);
             MetricsLogger.histogram(mContext,
-                    oldDream.mCanDoze ? "dozing_minutes" : "dreaming_minutes" ,
-                    (int) ((SystemClock.elapsedRealtime() - mDreamStartTime) / (1000L * 60L)));
+                    dream.mCanDoze ? "dozing_minutes" : "dreaming_minutes",
+                    (int) ((SystemClock.elapsedRealtime() - dream.mDreamStartTime) / (1000L
+                            * 60L)));
 
-            mHandler.removeCallbacks(mStopUnconnectedDreamRunnable);
-            mHandler.removeCallbacks(mStopStubbornDreamRunnable);
-            mSavedStopReason = null;
+            mHandler.removeCallbacks(dream.mStopUnconnectedDreamRunnable);
+            mHandler.removeCallbacks(dream.mStopStubbornDreamRunnable);
 
-            if (oldDream.mSentStartBroadcast) {
-                mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
-            }
-
-            if (oldDream.mService != null) {
+            if (dream.mService != null) {
                 try {
-                    oldDream.mService.detach();
+                    dream.mService.detach();
                 } catch (RemoteException ex) {
                     // we don't care; this thing is on the way out
                 }
 
                 try {
-                    oldDream.mService.asBinder().unlinkToDeath(oldDream, 0);
+                    dream.mService.asBinder().unlinkToDeath(dream, 0);
                 } catch (NoSuchElementException ex) {
                     // don't care
                 }
-                oldDream.mService = null;
+                dream.mService = null;
             }
 
-            if (oldDream.mBound) {
-                mContext.unbindService(oldDream);
+            if (dream.mBound) {
+                mContext.unbindService(dream);
             }
-            oldDream.releaseWakeLockIfNeeded();
+            dream.releaseWakeLockIfNeeded();
 
-            mHandler.post(() -> mListener.onDreamStopped(oldDream.mToken));
+            // Current dream stopped, device no longer dreaming.
+            if (dream == mCurrentDream) {
+                mCurrentDream = null;
+
+                if (mSentStartBroadcast) {
+                    mContext.sendBroadcastAsUser(mDreamingStoppedIntent, UserHandle.ALL);
+                }
+
+                mActivityTaskManager.removeRootTasksWithActivityTypes(
+                        new int[] {ACTIVITY_TYPE_DREAM});
+
+                mListener.onDreamStopped(dream.mToken);
+            }
         } finally {
             Trace.traceEnd(Trace.TRACE_TAG_POWER);
         }
     }
 
+    /**
+     * Stops all previous dreams, if any.
+     */
+    private void stopPreviousDreams() {
+        if (mPreviousDreams.isEmpty()) {
+            return;
+        }
+
+        // Using an iterator because mPreviousDreams is modified while the iteration is in process.
+        for (final Iterator<DreamRecord> it = mPreviousDreams.iterator(); it.hasNext(); ) {
+            stopDreamInstance(true /*immediate*/, "stop previous dream", it.next());
+            it.remove();
+        }
+    }
+
     private void attach(IDreamService service) {
         try {
             service.asBinder().linkToDeath(mCurrentDream, 0);
@@ -248,9 +285,9 @@
 
         mCurrentDream.mService = service;
 
-        if (!mCurrentDream.mIsPreviewMode) {
+        if (!mCurrentDream.mIsPreviewMode && !mSentStartBroadcast) {
             mContext.sendBroadcastAsUser(mDreamingStartedIntent, UserHandle.ALL);
-            mCurrentDream.mSentStartBroadcast = true;
+            mSentStartBroadcast = true;
         }
     }
 
@@ -272,10 +309,35 @@
         public boolean mBound;
         public boolean mConnected;
         public IDreamService mService;
-        public boolean mSentStartBroadcast;
-
+        private String mStopReason;
+        private long mDreamStartTime;
         public boolean mWakingGently;
 
+        private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded;
+        private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
+
+        private final Runnable mStopUnconnectedDreamRunnable = () -> {
+            if (mBound && !mConnected) {
+                Slog.w(TAG, "Bound dream did not connect in the time allotted");
+                stopDream(true /*immediate*/, "slow to connect" /*reason*/);
+            }
+        };
+
+        private final Runnable mStopStubbornDreamRunnable = () -> {
+            Slog.w(TAG, "Stubborn dream did not finish itself in the time allotted");
+            stopDream(true /*immediate*/, "slow to finish" /*reason*/);
+            mStopReason = null;
+        };
+
+        private final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
+            // May be called on any thread.
+            @Override
+            public void sendResult(Bundle data) {
+                mHandler.post(mStopPreviousDreamsIfNeeded);
+                mHandler.post(mReleaseWakeLockIfNeeded);
+            }
+        };
+
         DreamRecord(Binder token, ComponentName name, boolean isPreviewMode,
                 boolean canDoze, int userId, PowerManager.WakeLock wakeLock) {
             mToken = token;
@@ -286,7 +348,9 @@
             mWakeLock = wakeLock;
             // Hold the lock while we're waiting for the service to connect and start dreaming.
             // Released after the service has started dreaming, we stop dreaming, or it timed out.
-            mWakeLock.acquire();
+            if (mWakeLock != null) {
+                mWakeLock.acquire();
+            }
             mHandler.postDelayed(mReleaseWakeLockIfNeeded, 10000);
         }
 
@@ -326,6 +390,12 @@
             });
         }
 
+        void stopPreviousDreamsIfNeeded() {
+            if (mCurrentDream == DreamRecord.this) {
+                stopPreviousDreams();
+            }
+        }
+
         void releaseWakeLockIfNeeded() {
             if (mWakeLock != null) {
                 mWakeLock.release();
@@ -333,15 +403,5 @@
                 mHandler.removeCallbacks(mReleaseWakeLockIfNeeded);
             }
         }
-
-        final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded;
-
-        final IRemoteCallback mDreamingStartedCallback = new IRemoteCallback.Stub() {
-            // May be called on any thread.
-            @Override
-            public void sendResult(Bundle data) throws RemoteException {
-                mHandler.post(mReleaseWakeLockIfNeeded);
-            }
-        };
     }
 }
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 951a8a2..6e2cceb 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -493,8 +493,6 @@
             return;
         }
 
-        stopDreamLocked(true /*immediate*/, "starting new dream");
-
         Slog.i(TAG, "Entering dreamland.");
 
         mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
index 3fecef7..88145bd 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
@@ -34,10 +34,10 @@
 import android.text.FontConfig;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.util.DumpUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
index 15abbd5..fd00980 100644
--- a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
+++ b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java
@@ -21,10 +21,11 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiUtils.java b/services/core/java/com/android/server/hdmi/HdmiUtils.java
index ba19cf0..573bf19 100644
--- a/services/core/java/com/android/server/hdmi/HdmiUtils.java
+++ b/services/core/java/com/android/server/hdmi/HdmiUtils.java
@@ -25,11 +25,11 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.hdmi.Constants.AbortReason;
 import com.android.server.hdmi.Constants.AudioCodec;
 import com.android.server.hdmi.Constants.FeatureOpcode;
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java
index 324eefc..36199de 100644
--- a/services/core/java/com/android/server/input/BatteryController.java
+++ b/services/core/java/com/android/server/input/BatteryController.java
@@ -44,6 +44,8 @@
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * A thread-safe component of {@link InputManagerService} responsible for managing the battery state
@@ -63,6 +65,8 @@
 
     @VisibleForTesting
     static final long POLLING_PERIOD_MILLIS = 10_000; // 10 seconds
+    @VisibleForTesting
+    static final long USI_BATTERY_VALIDITY_DURATION_MILLIS = 60 * 60_000; // 1 hour
 
     private final Object mLock = new Object();
     private final Context mContext;
@@ -98,8 +102,12 @@
     }
 
     public void systemRunning() {
-        Objects.requireNonNull(mContext.getSystemService(InputManager.class))
-                .registerInputDeviceListener(mInputDeviceListener, mHandler);
+        final InputManager inputManager =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+        inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
+        for (int deviceId : inputManager.getInputDeviceIds()) {
+            mInputDeviceListener.onInputDeviceAdded(deviceId);
+        }
     }
 
     /**
@@ -165,19 +173,20 @@
         }
     }
 
-    @GuardedBy("mLock")
-    private void notifyAllListenersForDeviceLocked(State state) {
-        if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
-        mListenerRecords.forEach((pid, listenerRecord) -> {
-            if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
-                notifyBatteryListener(listenerRecord, state);
-            }
-        });
+    private void notifyAllListenersForDevice(State state) {
+        synchronized (mLock) {
+            if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
+            mListenerRecords.forEach((pid, listenerRecord) -> {
+                if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
+                    notifyBatteryListener(listenerRecord, state);
+                }
+            });
+        }
     }
 
     @GuardedBy("mLock")
     private void updatePollingLocked(boolean delayStart) {
-        if (mDeviceMonitors.isEmpty() || !mIsInteractive) {
+        if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) {
             // Stop polling.
             mIsPolling = false;
             mHandler.removeCallbacks(this::handlePollEvent);
@@ -192,6 +201,13 @@
         mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
     }
 
+    private String getInputDeviceName(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null ? device.getName() : "<none>";
+    }
+
     private boolean hasBattery(int deviceId) {
         final InputDevice device =
                 Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -199,6 +215,13 @@
         return device != null && device.hasBattery();
     }
 
+    private boolean isUsiDevice(int deviceId) {
+        final InputDevice device =
+                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
+                        .getInputDevice(deviceId);
+        return device != null && device.supportsUsi();
+    }
+
     @GuardedBy("mLock")
     private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) {
         return Objects.requireNonNull(mDeviceMonitors.get(deviceId),
@@ -252,8 +275,10 @@
         if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
             // There are no more listeners monitoring this device.
             final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId);
-            monitor.stopMonitoring();
-            mDeviceMonitors.remove(deviceId);
+            if (!monitor.isPersistent()) {
+                monitor.onMonitorDestroy();
+                mDeviceMonitors.remove(deviceId);
+            }
         }
 
         if (listenerRecord.mMonitoredDevices.isEmpty()) {
@@ -298,9 +323,7 @@
             if (monitor == null) {
                 return;
             }
-            if (monitor.updateBatteryState(eventTime)) {
-                notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-            }
+            monitor.onUEvent(eventTime);
         }
     }
 
@@ -310,18 +333,22 @@
                 return;
             }
             final long eventTime = SystemClock.uptimeMillis();
-            mDeviceMonitors.forEach((deviceId, monitor) -> {
-                // Re-acquire lock in the lambda to silence error-prone build warnings.
-                synchronized (mLock) {
-                    if (monitor.updateBatteryState(eventTime)) {
-                        notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-                    }
-                }
-            });
+            mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime));
             mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS);
         }
     }
 
+    private void handleMonitorTimeout(int deviceId) {
+        synchronized (mLock) {
+            final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
+            if (monitor == null) {
+                return;
+            }
+            final long updateTime = SystemClock.uptimeMillis();
+            monitor.onTimeout(updateTime);
+        }
+    }
+
     /** Gets the current battery state of an input device. */
     public IInputDeviceBatteryState getBatteryState(int deviceId) {
         synchronized (mLock) {
@@ -329,15 +356,11 @@
             final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
             if (monitor == null) {
                 // The input device's battery is not being monitored by any listener.
-                return queryBatteryStateFromNative(deviceId, updateTime);
+                return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId));
             }
             // Force the battery state to update, and notify listeners if necessary.
-            final boolean stateChanged = monitor.updateBatteryState(updateTime);
-            final State state = monitor.getBatteryStateForReporting();
-            if (stateChanged) {
-                notifyAllListenersForDeviceLocked(state);
-            }
-            return state;
+            monitor.onPoll(updateTime);
+            return monitor.getBatteryStateForReporting();
         }
     }
 
@@ -379,7 +402,14 @@
     private final InputManager.InputDeviceListener mInputDeviceListener =
             new InputManager.InputDeviceListener() {
         @Override
-        public void onInputDeviceAdded(int deviceId) {}
+        public void onInputDeviceAdded(int deviceId) {
+            synchronized (mLock) {
+                if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) {
+                    // Start monitoring USI device immediately.
+                    mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId));
+                }
+            }
+        }
 
         @Override
         public void onInputDeviceRemoved(int deviceId) {}
@@ -392,9 +422,7 @@
                     return;
                 }
                 final long eventTime = SystemClock.uptimeMillis();
-                if (monitor.updateBatteryState(eventTime)) {
-                    notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
-                }
+                monitor.onConfiguration(eventTime);
             }
         }
     };
@@ -422,8 +450,7 @@
     }
 
     // Queries the battery state of an input device from native code.
-    private State queryBatteryStateFromNative(int deviceId, long updateTime) {
-        final boolean isPresent = hasBattery(deviceId);
+    private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) {
         return new State(
                 deviceId,
                 updateTime,
@@ -434,8 +461,9 @@
 
     // Holds the state of an InputDevice for which battery changes are currently being monitored.
     private class DeviceMonitor {
-        @NonNull
-        private State mState;
+        protected final State mState;
+        // Represents whether the input device has a sysfs battery node.
+        protected boolean mHasBattery = false;
 
         @Nullable
         private UEventBatteryListener mUEventBatteryListener;
@@ -445,26 +473,32 @@
 
             // Load the initial battery state and start monitoring.
             final long eventTime = SystemClock.uptimeMillis();
-            updateBatteryState(eventTime);
+            configureDeviceMonitor(eventTime);
         }
 
-        // Returns true if the battery state changed since the last time it was updated.
-        public boolean updateBatteryState(long updateTime) {
-            mState.updateTime = updateTime;
-
-            final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime);
-            if (mState.equals(updatedState)) {
-                return false;
+        protected void processChangesAndNotify(long eventTime, Consumer<Long> changes) {
+            final State oldState = getBatteryStateForReporting();
+            changes.accept(eventTime);
+            final State newState = getBatteryStateForReporting();
+            if (!oldState.equals(newState)) {
+                notifyAllListenersForDevice(newState);
             }
-            if (mState.isPresent != updatedState.isPresent) {
-                if (updatedState.isPresent) {
+        }
+
+        public void onConfiguration(long eventTime) {
+            processChangesAndNotify(eventTime, this::configureDeviceMonitor);
+        }
+
+        private void configureDeviceMonitor(long eventTime) {
+            if (mHasBattery != hasBattery(mState.deviceId)) {
+                mHasBattery = !mHasBattery;
+                if (mHasBattery) {
                     startMonitoring();
                 } else {
                     stopMonitoring();
                 }
+                updateBatteryStateFromNative(eventTime);
             }
-            mState = updatedState;
-            return true;
         }
 
         private void startMonitoring() {
@@ -483,19 +517,46 @@
                     mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath));
         }
 
-        private String formatDevPath(String path) {
+        private String formatDevPath(@NonNull String path) {
             // Remove the "/sys" prefix if it has one.
             return path.startsWith("/sys") ? path.substring(4) : path;
         }
 
-        // This must be called when the device is no longer being monitored.
-        public void stopMonitoring() {
+        private void stopMonitoring() {
             if (mUEventBatteryListener != null) {
                 mUEventManager.removeListener(mUEventBatteryListener);
                 mUEventBatteryListener = null;
             }
         }
 
+        // This must be called when the device is no longer being monitored.
+        public void onMonitorDestroy() {
+            stopMonitoring();
+        }
+
+        protected void updateBatteryStateFromNative(long eventTime) {
+            mState.updateIfChanged(
+                    queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery));
+        }
+
+        public void onPoll(long eventTime) {
+            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
+        }
+
+        public void onUEvent(long eventTime) {
+            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
+        }
+
+        public boolean requiresPolling() {
+            return true;
+        }
+
+        public boolean isPersistent() {
+            return false;
+        }
+
+        public void onTimeout(long eventTime) {}
+
         // Returns the current battery state that can be used to notify listeners BatteryController.
         public State getBatteryStateForReporting() {
             return new State(mState);
@@ -503,8 +564,98 @@
 
         @Override
         public String toString() {
-            return "state=" + mState
-                    + ", uEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+            return "DeviceId=" + mState.deviceId
+                    + ", Name='" + getInputDeviceName(mState.deviceId) + "'"
+                    + ", NativeBattery=" + mState
+                    + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+        }
+    }
+
+    // Battery monitoring logic that is specific to stylus devices that support the
+    // Universal Stylus Initiative (USI) protocol.
+    private class UsiDeviceMonitor extends DeviceMonitor {
+
+        // For USI devices, we only treat the battery state as valid for a fixed amount of time
+        // after receiving a battery update. Once the timeout has passed, we signal to all listeners
+        // that there is no longer a battery present for the device. The battery state is valid
+        // as long as this callback is non-null.
+        @Nullable
+        private Runnable mValidityTimeoutCallback;
+
+        UsiDeviceMonitor(int deviceId) {
+            super(deviceId);
+        }
+
+        @Override
+        public void onPoll(long eventTime) {
+            // Disregard polling for USI devices.
+        }
+
+        @Override
+        public void onUEvent(long eventTime) {
+            processChangesAndNotify(eventTime, (time) -> {
+                updateBatteryStateFromNative(time);
+                markUsiBatteryValid();
+            });
+        }
+
+        @Override
+        public void onTimeout(long eventTime) {
+            processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid());
+        }
+
+        @Override
+        public void onConfiguration(long eventTime) {
+            super.onConfiguration(eventTime);
+
+            if (!mHasBattery) {
+                throw new IllegalStateException(
+                        "UsiDeviceMonitor: USI devices are always expected to "
+                                + "report a valid battery, but no battery was detected!");
+            }
+        }
+
+        private void markUsiBatteryValid() {
+            if (mValidityTimeoutCallback != null) {
+                mHandler.removeCallbacks(mValidityTimeoutCallback);
+            } else {
+                final int deviceId = mState.deviceId;
+                mValidityTimeoutCallback =
+                        () -> BatteryController.this.handleMonitorTimeout(deviceId);
+            }
+            mHandler.postDelayed(mValidityTimeoutCallback, USI_BATTERY_VALIDITY_DURATION_MILLIS);
+        }
+
+        private void markUsiBatteryInvalid() {
+            if (mValidityTimeoutCallback == null) {
+                return;
+            }
+            mHandler.removeCallbacks(mValidityTimeoutCallback);
+            mValidityTimeoutCallback = null;
+        }
+
+        @Override
+        public State getBatteryStateForReporting() {
+            return mValidityTimeoutCallback != null
+                    ? new State(mState) : new State(mState.deviceId);
+        }
+
+        @Override
+        public boolean requiresPolling() {
+            // Do not poll the battery state for USI devices.
+            return false;
+        }
+
+        @Override
+        public boolean isPersistent() {
+            // Do not remove the battery monitor for USI devices.
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString()
+                    + ", UsiStateIsValid=" + (mValidityTimeoutCallback != null);
         }
     }
 
@@ -548,18 +699,33 @@
     private static class State extends IInputDeviceBatteryState {
 
         State(int deviceId) {
-            initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
-                    Float.NaN /*capacity*/);
+            reset(deviceId);
         }
 
         State(IInputDeviceBatteryState s) {
-            initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
+            copyFrom(s);
         }
 
         State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) {
             initialize(deviceId, updateTime, isPresent, status, capacity);
         }
 
+        // Updates this from other if there is a difference between them, ignoring the updateTime.
+        public void updateIfChanged(IInputDeviceBatteryState other) {
+            if (!equalsIgnoringUpdateTime(other)) {
+                copyFrom(other);
+            }
+        }
+
+        public void reset(int deviceId) {
+            initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
+                    Float.NaN /*capacity*/);
+        }
+
+        private void copyFrom(IInputDeviceBatteryState s) {
+            initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
+        }
+
         private void initialize(int deviceId, long updateTime, boolean isPresent, int status,
                 float capacity) {
             this.deviceId = deviceId;
@@ -569,11 +735,34 @@
             this.capacity = capacity;
         }
 
+        private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) {
+            long updateTime = this.updateTime;
+            this.updateTime = other.updateTime;
+            boolean eq = this.equals(other);
+            this.updateTime = updateTime;
+            return eq;
+        }
+
         @Override
         public String toString() {
-            return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime
-                    + ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity
-                    + " }";
+            if (!isPresent) {
+                return "State{<not present>}";
+            }
+            return "State{time=" + updateTime
+                    + ", isPresent=" + isPresent
+                    + ", status=" + status
+                    + ", capacity=" + capacity
+                    + "}";
         }
     }
+
+    // Check if any value in an ArrayMap matches the predicate in an optimized way.
+    private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
+        for (int i = 0; i < arrayMap.size(); i++) {
+            if (test.test(arrayMap.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/input/ConfigurationProcessor.java b/services/core/java/com/android/server/input/ConfigurationProcessor.java
index 0563806..b6953a3 100644
--- a/services/core/java/com/android/server/input/ConfigurationProcessor.java
+++ b/services/core/java/com/android/server/input/ConfigurationProcessor.java
@@ -18,11 +18,11 @@
 
 import android.text.TextUtils;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import java.io.InputStream;
 import java.util.ArrayList;
diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java
index 5513cd6..1bb10c7 100644
--- a/services/core/java/com/android/server/input/PersistentDataStore.java
+++ b/services/core/java/com/android/server/input/PersistentDataStore.java
@@ -21,8 +21,6 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Surface;
 
@@ -34,6 +32,9 @@
 
 import org.xmlpull.v1.XmlPullParserException;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
index 816d08a..8e6452b 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
@@ -25,12 +25,13 @@
 import android.util.ArrayMap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 76331fd..76495b1 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -58,7 +58,6 @@
 import android.accessibilityservice.AccessibilityService;
 import android.annotation.AnyThread;
 import android.annotation.BinderThread;
-import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.DurationMillisLong;
 import android.annotation.EnforcePermission;
@@ -69,9 +68,6 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentProvider;
@@ -94,7 +90,6 @@
 import android.media.AudioManagerInternal;
 import android.net.Uri;
 import android.os.Binder;
-import android.os.Bundle;
 import android.os.Debug;
 import android.os.Handler;
 import android.os.IBinder;
@@ -170,8 +165,6 @@
 import com.android.internal.inputmethod.StartInputFlags;
 import com.android.internal.inputmethod.StartInputReason;
 import com.android.internal.inputmethod.UnbindReason;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.TransferPipe;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ConcurrentUtils;
@@ -255,13 +248,6 @@
     private static final String HANDLER_THREAD_NAME = "android.imms";
 
     /**
-     * A protected broadcast intent action for internal use for {@link PendingIntent} in
-     * the notification.
-     */
-    private static final String ACTION_SHOW_INPUT_METHOD_PICKER =
-            "com.android.server.inputmethod.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER";
-
-    /**
      * When set, {@link #startInputUncheckedLocked} will return
      * {@link InputBindResult#NO_EDITOR} instead of starting an IME connection
      * unless {@link StartInputFlags#IS_TEXT_EDITOR} is set. This behavior overrides
@@ -334,13 +320,8 @@
     @GuardedBy("ImfLock.class")
     private int mDisplayIdToShowIme = INVALID_DISPLAY;
 
-    // Ongoing notification
-    private NotificationManager mNotificationManager;
     @Nullable private StatusBarManagerInternal mStatusBarManagerInternal;
-    private final Notification.Builder mImeSwitcherNotification;
-    private final PendingIntent mImeSwitchPendingIntent;
     private boolean mShowOngoingImeSwitcherForPhones;
-    private boolean mNotificationShown;
     @GuardedBy("ImfLock.class")
     private final HandwritingModeController mHwController;
     @GuardedBy("ImfLock.class")
@@ -1253,17 +1234,6 @@
                 return;
             } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
                 onActionLocaleChanged();
-            } else if (ACTION_SHOW_INPUT_METHOD_PICKER.equals(action)) {
-                // ACTION_SHOW_INPUT_METHOD_PICKER action is a protected-broadcast and it is
-                // guaranteed to be send only from the system, so that there is no need for extra
-                // security check such as
-                // {@link #canShowInputMethodPickerLocked(IInputMethodClient)}.
-                mHandler.obtainMessage(
-                        MSG_SHOW_IM_SUBTYPE_PICKER,
-                        // TODO(b/120076400): Design and implement IME switcher for heterogeneous
-                        // navbar configuration.
-                        InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES,
-                        DEFAULT_DISPLAY).sendToTarget();
             } else {
                 Slog.w(TAG, "Unexpected intent " + intent);
             }
@@ -1720,27 +1690,8 @@
 
         mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
 
-        Bundle extras = new Bundle();
-        extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
-        @ColorInt final int accentColor = mContext.getColor(
-                com.android.internal.R.color.system_notification_accent_color);
-        mImeSwitcherNotification =
-                new Notification.Builder(mContext, SystemNotificationChannels.VIRTUAL_KEYBOARD)
-                        .setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default)
-                        .setWhen(0)
-                        .setOngoing(true)
-                        .addExtras(extras)
-                        .setCategory(Notification.CATEGORY_SYSTEM)
-                        .setColor(accentColor);
-
-        Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER)
-                .setPackage(mContext.getPackageName());
-        mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent,
-                PendingIntent.FLAG_IMMUTABLE);
-
         mShowOngoingImeSwitcherForPhones = false;
 
-        mNotificationShown = false;
         final int userId = mActivityManagerInternal.getCurrentUserId();
 
         mLastSwitchUserId = userId;
@@ -1939,7 +1890,6 @@
                 final int currentUserId = mSettings.getCurrentUserId();
                 mSettings.switchCurrentUser(currentUserId,
                         !mUserManagerInternal.isUserUnlockingOrUnlocked(currentUserId));
-                mNotificationManager = mContext.getSystemService(NotificationManager.class);
                 mStatusBarManagerInternal =
                         LocalServices.getService(StatusBarManagerInternal.class);
                 hideStatusBarIconLocked();
@@ -1977,7 +1927,6 @@
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_ADDED);
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_USER_REMOVED);
                 broadcastFilterForSystemUser.addAction(Intent.ACTION_LOCALE_CHANGED);
-                broadcastFilterForSystemUser.addAction(ACTION_SHOW_INPUT_METHOD_PICKER);
                 mContext.registerReceiver(new ImmsBroadcastReceiverForSystemUser(),
                         broadcastFilterForSystemUser);
 
@@ -3159,41 +3108,6 @@
                 mStatusBarManagerInternal.setImeWindowStatus(mCurTokenDisplayId,
                         getCurTokenLocked(), vis, backDisposition, needsToShowImeSwitcher);
             }
-            final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
-            if (imi != null && needsToShowImeSwitcher) {
-                // Used to load label
-                final CharSequence title = mRes.getText(
-                        com.android.internal.R.string.select_input_method);
-                final int currentUserId = mSettings.getCurrentUserId();
-                final Context userAwareContext = mContext.getUserId() == currentUserId
-                        ? mContext
-                        : mContext.createContextAsUser(UserHandle.of(currentUserId), 0 /* flags */);
-                final CharSequence summary = InputMethodUtils.getImeAndSubtypeDisplayName(
-                        userAwareContext, imi, mCurrentSubtype);
-                mImeSwitcherNotification.setContentTitle(title)
-                        .setContentText(summary)
-                        .setContentIntent(mImeSwitchPendingIntent);
-                // TODO(b/120076400): Figure out what is the best behavior
-                if ((mNotificationManager != null)
-                        && !mWindowManagerInternal.hasNavigationBar(DEFAULT_DISPLAY)) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "--- show notification: label =  " + summary);
-                    }
-                    mNotificationManager.notifyAsUser(null,
-                            SystemMessage.NOTE_SELECT_INPUT_METHOD,
-                            mImeSwitcherNotification.build(), UserHandle.ALL);
-                    mNotificationShown = true;
-                }
-            } else {
-                if (mNotificationShown && mNotificationManager != null) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "--- hide notification");
-                    }
-                    mNotificationManager.cancelAsUser(null,
-                            SystemMessage.NOTE_SELECT_INPUT_METHOD, UserHandle.ALL);
-                    mNotificationShown = false;
-                }
-            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
index c7ff8ca..ebf9237d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java
@@ -179,16 +179,6 @@
         }
     }
 
-    static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
-            InputMethodSubtype subtype) {
-        final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
-        return subtype != null
-                ? TextUtils.concat(subtype.getDisplayName(context,
-                        imi.getPackageName(), imi.getServiceInfo().applicationInfo),
-                                (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel))
-                : imiLabel;
-    }
-
     /**
      * Returns true if a package name belongs to a UID.
      *
diff --git a/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java b/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java
index ab91290..e831e40 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleMetadataParser.java
@@ -17,9 +17,9 @@
 package com.android.server.integrity.parser;
 
 import android.annotation.Nullable;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.integrity.model.RuleMetadata;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java
index 7aed352..022b4b8 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleMetadataSerializer.java
@@ -19,9 +19,9 @@
 import static com.android.server.integrity.parser.RuleMetadataParser.RULE_PROVIDER_TAG;
 import static com.android.server.integrity.parser.RuleMetadataParser.VERSION_TAG;
 
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.integrity.model.RuleMetadata;
 
 import org.xmlpull.v1.XmlSerializer;
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index 37a4869..67c931f 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -37,12 +37,12 @@
 import android.text.TextUtils;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java b/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java
index d13b1f4..215c653 100644
--- a/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java
+++ b/services/core/java/com/android/server/locales/SystemAppUpdateTracker.java
@@ -28,12 +28,12 @@
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
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 7ce1017..51851be 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -122,23 +122,23 @@
 
     private final Context mContext;
 
-    private final Map<Integer, ContextHubInfo> mContextHubIdToInfoMap;
-    private final List<String> mSupportedContextHubPerms;
-    private final List<ContextHubInfo> mContextHubInfoList;
+    private Map<Integer, ContextHubInfo> mContextHubIdToInfoMap;
+    private List<String> mSupportedContextHubPerms;
+    private List<ContextHubInfo> mContextHubInfoList;
     private final RemoteCallbackList<IContextHubCallback> mCallbacksList =
             new RemoteCallbackList<>();
 
     // Proxy object to communicate with the Context Hub HAL
-    private final IContextHubWrapper mContextHubWrapper;
+    private IContextHubWrapper mContextHubWrapper;
 
     // The manager for transaction queue
-    private final ContextHubTransactionManager mTransactionManager;
+    private ContextHubTransactionManager mTransactionManager;
 
     // The manager for sending messages to/from clients
-    private final ContextHubClientManager mClientManager;
+    private ContextHubClientManager mClientManager;
 
     // The default client for old API clients
-    private final Map<Integer, IContextHubClient> mDefaultClientMap;
+    private Map<Integer, IContextHubClient> mDefaultClientMap;
 
     // The manager for the internal nanoapp state cache
     private final NanoAppStateManager mNanoAppStateManager = new NanoAppStateManager();
@@ -167,7 +167,7 @@
     // Lock object for sendWifiSettingUpdate()
     private final Object mSendWifiSettingUpdateLock = new Object();
 
-    private final SensorPrivacyManagerInternal mSensorPrivacyManagerInternal;
+    private SensorPrivacyManagerInternal mSensorPrivacyManagerInternal;
 
     private final Map<Integer, AtomicLong> mLastRestartTimestampMap = new HashMap<>();
 
@@ -209,156 +209,9 @@
         }
     }
 
-    public ContextHubService(Context context) {
-        long startTimeNs = SystemClock.elapsedRealtimeNanos();
+    public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
         mContext = context;
-
-        mContextHubWrapper = getContextHubWrapper();
-        if (mContextHubWrapper == null) {
-            mTransactionManager = null;
-            mClientManager = null;
-            mSensorPrivacyManagerInternal = null;
-            mDefaultClientMap = Collections.emptyMap();
-            mContextHubIdToInfoMap = Collections.emptyMap();
-            mSupportedContextHubPerms = Collections.emptyList();
-            mContextHubInfoList = Collections.emptyList();
-            return;
-        }
-
-        Pair<List<ContextHubInfo>, List<String>> hubInfo;
-        try {
-            hubInfo = mContextHubWrapper.getHubs();
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException while getting Context Hub info", e);
-            hubInfo = new Pair(Collections.emptyList(), Collections.emptyList());
-        }
-        long bootTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
-        int numContextHubs = hubInfo.first.size();
-        ContextHubStatsLog.write(ContextHubStatsLog.CONTEXT_HUB_BOOTED, bootTimeNs, numContextHubs);
-
-        mContextHubIdToInfoMap = Collections.unmodifiableMap(
-                ContextHubServiceUtil.createContextHubInfoMap(hubInfo.first));
-        mSupportedContextHubPerms = hubInfo.second;
-        mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values());
-        mClientManager = new ContextHubClientManager(mContext, mContextHubWrapper);
-        mTransactionManager = new ContextHubTransactionManager(
-                mContextHubWrapper, mClientManager, mNanoAppStateManager);
-        mSensorPrivacyManagerInternal =
-                LocalServices.getService(SensorPrivacyManagerInternal.class);
-
-        HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>();
-        for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
-            mLastRestartTimestampMap.put(contextHubId,
-                    new AtomicLong(SystemClock.elapsedRealtimeNanos()));
-
-            ContextHubInfo contextHubInfo = mContextHubIdToInfoMap.get(contextHubId);
-            IContextHubClient client = mClientManager.registerClient(
-                    contextHubInfo, createDefaultClientCallback(contextHubId),
-                    null /* attributionTag */, mTransactionManager, mContext.getPackageName());
-            defaultClientMap.put(contextHubId, client);
-
-            try {
-                mContextHubWrapper.registerCallback(
-                        contextHubId, new ContextHubServiceCallback(contextHubId));
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException while registering service callback for hub (ID = "
-                        + contextHubId + ")", e);
-            }
-
-            // Do a query to initialize the service cache list of nanoapps
-            // TODO(b/69270990): Remove this when old API is deprecated
-            queryNanoAppsInternal(contextHubId);
-        }
-        mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
-
-        if (mContextHubWrapper.supportsLocationSettingNotifications()) {
-            sendLocationSettingUpdate();
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE),
-                    true /* notifyForDescendants */,
-                    new ContentObserver(null /* handler */) {
-                        @Override
-                        public void onChange(boolean selfChange) {
-                            sendLocationSettingUpdate();
-                        }
-                    }, UserHandle.USER_ALL);
-        }
-
-        if (mContextHubWrapper.supportsWifiSettingNotifications()) {
-            sendWifiSettingUpdate(true /* forceUpdate */);
-
-            BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())
-                            || WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED.equals(
-                            intent.getAction())) {
-                        sendWifiSettingUpdate(false /* forceUpdate */);
-                    }
-                }
-            };
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
-            filter.addAction(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED);
-            mContext.registerReceiver(wifiReceiver, filter);
-
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE),
-                    true /* notifyForDescendants */,
-                    new ContentObserver(null /* handler */) {
-                        @Override
-                        public void onChange(boolean selfChange) {
-                            sendWifiSettingUpdate(false /* forceUpdate */);
-                        }
-                    }, UserHandle.USER_ALL);
-        }
-
-        if (mContextHubWrapper.supportsAirplaneModeSettingNotifications()) {
-            sendAirplaneModeSettingUpdate();
-            mContext.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
-                    true /* notifyForDescendants */,
-                    new ContentObserver(null /* handler */) {
-                        @Override
-                        public void onChange(boolean selfChange) {
-                            sendAirplaneModeSettingUpdate();
-                        }
-                    }, UserHandle.USER_ALL);
-        }
-
-        if (mContextHubWrapper.supportsMicrophoneSettingNotifications()) {
-            sendMicrophoneDisableSettingUpdateForCurrentUser();
-
-            mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
-                    SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
-                        if (userId == getCurrentUserId()) {
-                            Log.d(TAG, "User: " + userId + "mic privacy: " + enabled);
-                            sendMicrophoneDisableSettingUpdate(enabled);
-                        }
-                    });
-
-        }
-
-        if (mContextHubWrapper.supportsBtSettingNotifications()) {
-            sendBtSettingUpdate(true /* forceUpdate */);
-
-            BroadcastReceiver btReceiver = new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())
-                            || BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(
-                            intent.getAction())) {
-                        sendBtSettingUpdate(false /* forceUpdate */);
-                    }
-                }
-            };
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
-            filter.addAction(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
-            mContext.registerReceiver(btReceiver, filter);
-        }
-
-        scheduleDailyMetricSnapshot();
+        init(contextHubWrapper, /* isFirstInit= */ true);
     }
 
     /**
@@ -437,21 +290,209 @@
     }
 
     /**
-     * @return the IContextHubWrapper interface
+     * Initializes the private state of the ContextHubService
+     *
+     * @param startTimeNs               the start time when init was called
+     * @param isFirstInit               if true, this is the first time init is called - boot time
+     *
+     * @return      if mContextHubWrapper is not null and a full state init was done
      */
-    private IContextHubWrapper getContextHubWrapper() {
-        IContextHubWrapper wrapper = IContextHubWrapper.maybeConnectToAidl();
-        if (wrapper == null) {
-            wrapper = IContextHubWrapper.maybeConnectTo1_2();
-        }
-        if (wrapper == null) {
-            wrapper = IContextHubWrapper.maybeConnectTo1_1();
-        }
-        if (wrapper == null) {
-            wrapper = IContextHubWrapper.maybeConnectTo1_0();
+    private boolean initContextHubServiceState(long startTimeNs, boolean isFirstInit) {
+        if (mContextHubWrapper == null) {
+            mTransactionManager = null;
+            mClientManager = null;
+            mSensorPrivacyManagerInternal = null;
+            mDefaultClientMap = Collections.emptyMap();
+            mContextHubIdToInfoMap = Collections.emptyMap();
+            mSupportedContextHubPerms = Collections.emptyList();
+            mContextHubInfoList = Collections.emptyList();
+            return false;
         }
 
-        return wrapper;
+        Pair<List<ContextHubInfo>, List<String>> hubInfo;
+        try {
+            hubInfo = mContextHubWrapper.getHubs();
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException while getting Context Hub info", e);
+            hubInfo = new Pair(Collections.emptyList(), Collections.emptyList());
+        }
+
+        if (isFirstInit) {
+            long bootTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
+            int numContextHubs = hubInfo.first.size();
+            ContextHubStatsLog.write(ContextHubStatsLog.CONTEXT_HUB_BOOTED, bootTimeNs,
+                    numContextHubs);
+        }
+
+        mContextHubIdToInfoMap = Collections.unmodifiableMap(
+                ContextHubServiceUtil.createContextHubInfoMap(hubInfo.first));
+        mSupportedContextHubPerms = hubInfo.second;
+        mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values());
+        mClientManager = new ContextHubClientManager(mContext, mContextHubWrapper);
+        mTransactionManager = new ContextHubTransactionManager(
+                mContextHubWrapper, mClientManager, mNanoAppStateManager);
+        mSensorPrivacyManagerInternal =
+                LocalServices.getService(SensorPrivacyManagerInternal.class);
+        return true;
+    }
+
+    /**
+     * Creates the default client map that maps context hub IDs to the associated
+     * ClientManager. The client map is unmodifiable
+     */
+    private void initDefaultClientMap() {
+        HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>();
+        for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
+            mLastRestartTimestampMap.put(contextHubId,
+                    new AtomicLong(SystemClock.elapsedRealtimeNanos()));
+
+            ContextHubInfo contextHubInfo = mContextHubIdToInfoMap.get(contextHubId);
+            IContextHubClient client = mClientManager.registerClient(
+                    contextHubInfo, createDefaultClientCallback(contextHubId),
+                    /* attributionTag= */ null, mTransactionManager, mContext.getPackageName());
+            defaultClientMap.put(contextHubId, client);
+
+            try {
+                mContextHubWrapper.registerCallback(contextHubId,
+                        new ContextHubServiceCallback(contextHubId));
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException while registering service callback for hub (ID = "
+                        + contextHubId + ")", e);
+            }
+
+            // Do a query to initialize the service cache list of nanoapps
+            // TODO(b/194289715): Remove this when old API is deprecated
+            queryNanoAppsInternal(contextHubId);
+        }
+        mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap);
+    }
+
+    /**
+     * Handles the initialization of location settings notifications
+     */
+    private void initLocationSettingNotifications() {
+        if (mContextHubWrapper == null
+                || !mContextHubWrapper.supportsLocationSettingNotifications()) {
+            return;
+        }
+
+        sendLocationSettingUpdate();
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCATION_MODE),
+                /* notifyForDescendants= */ true,
+                new ContentObserver(/* handler= */ null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        sendLocationSettingUpdate();
+                    }
+                }, UserHandle.USER_ALL);
+    }
+
+    /**
+     * Handles the initialization of wifi settings notifications
+     */
+    private void initWifiSettingNotifications() {
+        if (mContextHubWrapper == null || !mContextHubWrapper.supportsWifiSettingNotifications()) {
+            return;
+        }
+
+        sendWifiSettingUpdate(/* forceUpdate= */ true);
+
+        BroadcastReceiver wifiReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())
+                        || WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED.equals(
+                        intent.getAction())) {
+                    sendWifiSettingUpdate(/* forceUpdate= */ false);
+                }
+            }
+        };
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
+        filter.addAction(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED);
+        mContext.registerReceiver(wifiReceiver, filter);
+
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE),
+                /* notifyForDescendants= */ true,
+                new ContentObserver(/* handler= */ null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        sendWifiSettingUpdate(/* forceUpdate= */ false);
+                    }
+                }, UserHandle.USER_ALL);
+    }
+
+    /**
+     * Handles the initialization of airplane mode settings notifications
+     */
+    private void initAirplaneModeSettingNotifications() {
+        if (mContextHubWrapper == null
+                || !mContextHubWrapper.supportsAirplaneModeSettingNotifications()) {
+            return;
+        }
+
+        sendAirplaneModeSettingUpdate();
+        mContext.getContentResolver().registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
+                /* notifyForDescendants= */ true,
+                new ContentObserver(/* handler= */ null) {
+                    @Override
+                    public void onChange(boolean selfChange) {
+                        sendAirplaneModeSettingUpdate();
+                    }
+                }, UserHandle.USER_ALL);
+    }
+
+    /**
+     * Handles the initialization of microphone settings notifications
+     */
+    private void initMicrophoneSettingNotifications() {
+        if (mContextHubWrapper == null
+                || !mContextHubWrapper.supportsMicrophoneSettingNotifications()) {
+            return;
+        }
+
+        sendMicrophoneDisableSettingUpdateForCurrentUser();
+        if (mSensorPrivacyManagerInternal == null) {
+            Log.e(TAG, "Unable to add a sensor privacy listener for all users");
+            return;
+        }
+
+        mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
+                SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
+                    if (userId == getCurrentUserId()) {
+                        Log.d(TAG, "User: " + userId + "mic privacy: " + enabled);
+                        sendMicrophoneDisableSettingUpdate(enabled);
+                    }
+                });
+    }
+
+    /**
+     * Handles the initialization of bluetooth settings notifications
+     */
+    private void initBtSettingNotifications() {
+        if (mContextHubWrapper == null || !mContextHubWrapper.supportsBtSettingNotifications()) {
+            return;
+        }
+
+        sendBtSettingUpdate(/* forceUpdate= */ true);
+
+        BroadcastReceiver btReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())
+                        || BluetoothAdapter.ACTION_BLE_STATE_CHANGED.equals(
+                        intent.getAction())) {
+                    sendBtSettingUpdate(/* forceUpdate= */ false);
+                }
+            }
+        };
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
+        filter.addAction(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
+        mContext.registerReceiver(btReceiver, filter);
     }
 
     @Override
@@ -708,6 +749,31 @@
     }
 
     /**
+     * Handles a service restart or service init for the first time
+     *
+     * @param contextHubWrapper         the Context Hub wrapper
+     * @param isFirstInit               if true, this is the first time init is called - boot time
+     */
+    private void init(IContextHubWrapper contextHubWrapper, boolean isFirstInit) {
+        Log.i(TAG, "Starting Context Hub Service init");
+        long startTimeNs = SystemClock.elapsedRealtimeNanos();
+        mContextHubWrapper = contextHubWrapper;
+        if (!initContextHubServiceState(startTimeNs, isFirstInit)) {
+            Log.e(TAG, "Failed to initialize the Context Hub Service");
+            return;
+        }
+        initDefaultClientMap();
+
+        initLocationSettingNotifications();
+        initWifiSettingNotifications();
+        initAirplaneModeSettingNotifications();
+        initMicrophoneSettingNotifications();
+        initBtSettingNotifications();
+
+        scheduleDailyMetricSnapshot();
+    }
+
+    /**
      * Handles a unicast or broadcast message from a nanoapp.
      *
      * @param contextHubId the ID of the hub the message came from
@@ -729,7 +795,7 @@
 
     /**
      * A helper function to handle a load response from the Context Hub for the old API.
-     * TODO(b/69270990): Remove this once the old APIs are obsolete.
+     * TODO(b/194289715): Remove this once the old APIs are obsolete.
      */
     private void handleLoadResponseOldApi(
             int contextHubId, int result, NanoAppBinary nanoAppBinary) {
@@ -750,7 +816,7 @@
     /**
      * A helper function to handle an unload response from the Context Hub for the old API.
      * <p>
-     * TODO(b/69270990): Remove this once the old APIs are obsolete.
+     * TODO(b/194289715): Remove this once the old APIs are obsolete.
      */
     private void handleUnloadResponseOldApi(int contextHubId, int result) {
         byte[] data = new byte[1];
@@ -788,10 +854,10 @@
             ContextHubEventLogger.getInstance().logContextHubRestart(contextHubId);
 
             sendLocationSettingUpdate();
-            sendWifiSettingUpdate(true /* forceUpdate */);
+            sendWifiSettingUpdate(/* forceUpdate= */ true);
             sendAirplaneModeSettingUpdate();
             sendMicrophoneDisableSettingUpdateForCurrentUser();
-            sendBtSettingUpdate(true /* forceUpdate */);
+            sendBtSettingUpdate(/* forceUpdate= */ true);
 
             mTransactionManager.onHubReset();
             queryNanoAppsInternal(contextHubId);
@@ -1066,8 +1132,8 @@
         mClientManager.forEachClientOfHub(contextHubId, client -> {
             if (client.getPackageName().equals(packageName)) {
                 client.updateNanoAppAuthState(
-                        nanoAppId, Collections.emptyList() /* nanoappPermissions */,
-                        false /* gracePeriodExpired */, true /* forceDenied */);
+                        nanoAppId, /* nanoappPermissions= */ Collections.emptyList(),
+                        /* gracePeriodExpired= */ false, /* forceDenied= */ true);
             }
         });
     }
@@ -1151,7 +1217,7 @@
         }
         if (!isValidContextHubId(contextHubId)) {
             Log.e(TAG, "Cannot start "
-                    + ContextHubTransaction.typeToString(transactionType, false /* upperCase */)
+                    + ContextHubTransaction.typeToString(transactionType, /* upperCase= */ false)
                     + " transaction for invalid hub ID " + contextHubId);
             try {
                 callback.onTransactionComplete(ContextHubTransaction.RESULT_FAILED_BAD_PARAMS);
@@ -1260,7 +1326,8 @@
      * Hub.
      */
     private void sendMicrophoneDisableSettingUpdateForCurrentUser() {
-        boolean isEnabled = mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
+        boolean isEnabled = mSensorPrivacyManagerInternal == null ? false :
+                mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
                 getCurrentUserId(), SensorPrivacyManager.Sensors.MICROPHONE);
         sendMicrophoneDisableSettingUpdate(isEnabled);
     }
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index acc0746..432b097 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -95,6 +95,24 @@
     }
 
     /**
+     * @return the IContextHubWrapper interface
+     */
+    public static IContextHubWrapper getContextHubWrapper() {
+        IContextHubWrapper wrapper = maybeConnectToAidl();
+        if (wrapper == null) {
+            wrapper = maybeConnectTo1_2();
+        }
+        if (wrapper == null) {
+            wrapper = maybeConnectTo1_1();
+        }
+        if (wrapper == null) {
+            wrapper = maybeConnectTo1_0();
+        }
+
+        return wrapper;
+    }
+
+    /**
      * Attempts to connect to the Contexthub HAL 1.0 service, if it exists.
      *
      * @return A valid IContextHubWrapper if the connection was successful, null otherwise.
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java
index 0c209c5..2596cee 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java
@@ -45,9 +45,10 @@
 import android.security.keystore.recovery.KeyDerivationParams;
 import android.security.keystore.recovery.WrappedApplicationKey;
 import android.util.Base64;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java
index eb34e98..9e3da23 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java
@@ -46,9 +46,10 @@
 import android.security.keystore.recovery.KeyDerivationParams;
 import android.security.keystore.recovery.WrappedApplicationKey;
 import android.util.Base64;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlSerializer;
+
 import java.io.IOException;
 import java.io.OutputStream;
 import java.security.cert.CertPath;
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 77dbde1..f653b93 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1662,116 +1662,75 @@
         }
 
         private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) {
-            MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo();
-
+            MediaRoute2ProviderInfo newInfo = provider.getProviderInfo();
             int providerInfoIndex =
                     indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos);
-
-            MediaRoute2ProviderInfo prevInfo =
+            MediaRoute2ProviderInfo oldInfo =
                     providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex);
-
-            // Ignore if no changes
-            if (Objects.equals(prevInfo, currentInfo)) {
+            if (oldInfo == newInfo) {
+                // Nothing to do.
                 return;
             }
 
-            boolean hasAddedOrModifiedRoutes = false;
-            boolean hasRemovedRoutes = false;
-
-            boolean isSystemProvider = provider.mIsSystemRouteProvider;
-
-            if (prevInfo == null) {
-                // Provider is being added.
-                mLastProviderInfos.add(currentInfo);
-                addToRoutesMap(currentInfo.getRoutes(), isSystemProvider);
-                // Check if new provider exposes routes.
-                hasAddedOrModifiedRoutes = !currentInfo.getRoutes().isEmpty();
-            } else if (currentInfo == null) {
-                // Provider is being removed.
-                hasRemovedRoutes = true;
-                mLastProviderInfos.remove(prevInfo);
-                removeFromRoutesMap(prevInfo.getRoutes(), isSystemProvider);
-            } else {
-                // Provider is being updated.
-                mLastProviderInfos.set(providerInfoIndex, currentInfo);
-                final Collection<MediaRoute2Info> currentRoutes = currentInfo.getRoutes();
-
-                // Checking for individual routes.
-                for (MediaRoute2Info route : currentRoutes) {
-                    if (!route.isValid()) {
-                        Slog.w(
-                                TAG,
-                                "onProviderStateChangedOnHandler: Ignoring invalid route : "
-                                        + route);
-                        continue;
-                    }
-
-                    MediaRoute2Info prevRoute = prevInfo.getRoute(route.getOriginalId());
-                    if (prevRoute == null || !Objects.equals(prevRoute, route)) {
-                        hasAddedOrModifiedRoutes = true;
-                        mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route);
-                        if (!isSystemProvider) {
-                            mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route);
-                        }
-                    }
+            Collection<MediaRoute2Info> newRoutes;
+            Set<String> newRouteIds;
+            if (newInfo != null) {
+                // Adding or updating a provider.
+                newRoutes = newInfo.getRoutes();
+                newRouteIds =
+                        newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
+                if (providerInfoIndex >= 0) {
+                    mLastProviderInfos.set(providerInfoIndex, newInfo);
+                } else {
+                    mLastProviderInfos.add(newInfo);
                 }
+            } else /* newInfo == null */ {
+                // Removing a provider.
+                mLastProviderInfos.remove(oldInfo);
+                newRouteIds = Collections.emptySet();
+                newRoutes = Collections.emptySet();
+            }
 
-                // Checking for individual removals
-                for (MediaRoute2Info prevRoute : prevInfo.getRoutes()) {
-                    if (currentInfo.getRoute(prevRoute.getOriginalId()) == null) {
-                        hasRemovedRoutes = true;
-                        mLastNotifiedRoutesToPrivilegedRouters.remove(prevRoute.getId());
-                        if (!isSystemProvider) {
-                            mLastNotifiedRoutesToNonPrivilegedRouters.remove(prevRoute.getId());
-                        }
-                    }
+            // Add new routes to the maps.
+            boolean hasAddedOrModifiedRoutes = false;
+            for (MediaRoute2Info newRouteInfo : newRoutes) {
+                if (!newRouteInfo.isValid()) {
+                    Slog.w(TAG, "onProviderStateChangedOnHandler: Ignoring invalid route : "
+                            + newRouteInfo);
+                    continue;
+                }
+                if (!provider.mIsSystemRouteProvider) {
+                    mLastNotifiedRoutesToNonPrivilegedRouters.put(
+                            newRouteInfo.getId(), newRouteInfo);
+                }
+                MediaRoute2Info oldRouteInfo =
+                        mLastNotifiedRoutesToPrivilegedRouters.put(
+                                newRouteInfo.getId(), newRouteInfo);
+                hasAddedOrModifiedRoutes |=
+                        oldRouteInfo == null || !oldRouteInfo.equals(newRouteInfo);
+            }
+
+            // Remove stale routes from the maps.
+            Collection<MediaRoute2Info> oldRoutes =
+                    oldInfo == null ? Collections.emptyList() : oldInfo.getRoutes();
+            boolean hasRemovedRoutes = false;
+            for (MediaRoute2Info oldRoute : oldRoutes) {
+                String oldRouteId = oldRoute.getId();
+                if (!newRouteIds.contains(oldRouteId)) {
+                    hasRemovedRoutes = true;
+                    mLastNotifiedRoutesToPrivilegedRouters.remove(oldRouteId);
+                    mLastNotifiedRoutesToNonPrivilegedRouters.remove(oldRouteId);
                 }
             }
 
             dispatchUpdates(
                     hasAddedOrModifiedRoutes,
                     hasRemovedRoutes,
-                    isSystemProvider,
+                    provider.mIsSystemRouteProvider,
                     mSystemProvider.getDefaultRoute());
         }
 
         /**
-         * Adds provided routes to {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also adds them
-         * to {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were provided by a
-         * non-system route provider. Overwrites any route with matching id that already exists.
-         *
-         * @param routes list of routes to be added.
-         * @param isSystemRoutes indicates whether routes come from a system route provider.
-         */
-        private void addToRoutesMap(
-                @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) {
-            for (MediaRoute2Info route : routes) {
-                if (!isSystemRoutes) {
-                    mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route);
-                }
-                mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route);
-            }
-        }
-
-        /**
-         * Removes provided routes from {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also
-         * removes them from {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were
-         * provided by a non-system route provider.
-         *
-         * @param routes list of routes to be removed.
-         * @param isSystemRoutes whether routes come from a system route provider.
-         */
-        private void removeFromRoutesMap(
-                @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) {
-            for (MediaRoute2Info route : routes) {
-                if (!isSystemRoutes) {
-                    mLastNotifiedRoutesToNonPrivilegedRouters.remove(route.getId());
-                }
-                mLastNotifiedRoutesToPrivilegedRouters.remove(route.getId());
-            }
-        }
-
-        /**
          * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters}
          * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link
          * android.media.MediaRouter2 routers} and {@link MediaRouter2Manager managers} after a call
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index d770f71..56f3296 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -239,8 +239,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.SparseSetArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
@@ -255,6 +253,8 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.StatLogger;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.NetworkIdentityUtils;
 import com.android.net.module.util.NetworkStatsUtils;
 import com.android.net.module.util.PermissionUtils;
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
index 4506b7d..7da78f3 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistSettings.java
@@ -20,14 +20,14 @@
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3238f1f..3329f54 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -36,10 +36,10 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 
 import java.io.IOException;
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 4d55d4e..004caf3 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -60,14 +60,14 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.TriPredicate;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.NotificationManagerService.DumpFilter;
 import com.android.server.utils.TimingsTraceAndSlog;
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f459c0e..eb37ceb 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -258,8 +258,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 import android.view.accessibility.AccessibilityEvent;
@@ -291,6 +289,8 @@
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.TriPredicate;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.EventLogTags;
 import com.android.server.IoThread;
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index d8aa469..bbbf452 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -63,8 +63,6 @@
 import android.util.Slog;
 import android.util.SparseBooleanArray;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
@@ -73,6 +71,8 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.notification.PermissionHelper.PackagePermission;
 
 import org.json.JSONArray;
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 61936df..4bbd40d 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -30,12 +30,12 @@
 import android.util.IntArray;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.PackageManagerService;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 4c23ab8..4b2c88c 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -72,8 +72,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
@@ -82,6 +80,8 @@
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import libcore.io.IoUtils;
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 8e672c3..17bb39c 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -166,6 +166,14 @@
         CollectionUtils.addAll(updatedTargets, removeOverlaysForUser(
                 (info) -> !userPackages.containsKey(info.packageName), newUserId));
 
+        final ArraySet<String> overlaidByOthers = new ArraySet<>();
+        for (AndroidPackage androidPackage : userPackages.values()) {
+            final String overlayTarget = androidPackage.getOverlayTarget();
+            if (!TextUtils.isEmpty(overlayTarget)) {
+                overlaidByOthers.add(overlayTarget);
+            }
+        }
+
         // Update the state of all installed packages containing overlays, and initialize new
         // overlays that are not currently in the settings.
         for (int i = 0, n = userPackages.size(); i < n; i++) {
@@ -175,8 +183,10 @@
                         updatePackageOverlays(pkg, newUserId, 0 /* flags */));
 
                 // When a new user is switched to for the first time, package manager must be
-                // informed of the overlay paths for all packages installed in the user.
-                updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId));
+                // informed of the overlay paths for all overlaid packages installed in the user.
+                if (overlaidByOthers.contains(pkg.getPackageName())) {
+                    updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId));
+                }
             } catch (OperationFailedException e) {
                 Slog.e(TAG, "failed to initialize overlays of '" + pkg.getPackageName()
                         + "' for user " + newUserId + "", e);
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index 9e39226..eae614a 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -28,14 +28,14 @@
 import android.util.ArraySet;
 import android.util.Pair;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
index bb918d5..5e98cc0 100644
--- a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
+++ b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
@@ -35,9 +35,10 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 4e9c472..d6233c7 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -148,9 +148,18 @@
                         + intent.toShortString(false, true, false, false)
                         + " " + intent.getExtras(), here);
             }
+            final boolean ordered;
+            if (mAmInternal.isModernQueueEnabled()) {
+                // When the modern broadcast stack is enabled, deliver all our
+                // broadcasts as unordered, since the modern stack has better
+                // support for sequencing cold-starts, and it supports
+                // delivering resultTo for non-ordered broadcasts
+                ordered = false;
+            } else {
+                ordered = (finishedReceiver != null);
+            }
             mAmInternal.broadcastIntent(
-                    intent, finishedReceiver, requiredPermissions,
-                    finishedReceiver != null, userId,
+                    intent, finishedReceiver, requiredPermissions, ordered, userId,
                     broadcastAllowList == null ? null : broadcastAllowList.get(userId),
                     filterExtrasForReceiver, bOptions);
         }
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index a4e295b..bf00a33 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -203,6 +203,12 @@
     boolean filterSharedLibPackage(@Nullable PackageStateInternal ps, int uid, int userId,
             long flags);
     boolean isCallerSameApp(String packageName, int uid);
+    /**
+     * Returns true if the package name and the uid represent the same app.
+     *
+     * @param resolveIsolatedUid if true, resolves an isolated uid into the real uid.
+     */
+    boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid);
     boolean isComponentVisibleToInstantApp(@Nullable ComponentName component);
     boolean isComponentVisibleToInstantApp(@Nullable ComponentName component,
             @PackageManager.ComponentType int type);
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 5d479d5..b9967f9 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -114,7 +114,6 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -123,6 +122,7 @@
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.PackageDexUsage;
 import com.android.server.pm.parsing.PackageInfoUtils;
@@ -2209,11 +2209,19 @@
     }
 
     public final boolean isCallerSameApp(String packageName, int uid) {
+        return isCallerSameApp(packageName, uid, false /* resolveIsolatedUid */);
+    }
+
+    @Override
+    public final boolean isCallerSameApp(String packageName, int uid, boolean resolveIsolatedUid) {
         if (Process.isSdkSandboxUid(uid)) {
             return (packageName != null
                     && packageName.equals(mService.getSdkSandboxPackageName()));
         }
         AndroidPackage pkg = mPackages.get(packageName);
+        if (resolveIsolatedUid && Process.isIsolated(uid)) {
+            uid = getIsolatedOwner(uid);
+        }
         return pkg != null
                 && UserHandle.getAppId(uid) == pkg.getUid();
     }
diff --git a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
index 718756f..0cd698a 100644
--- a/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilter.java
@@ -21,10 +21,10 @@
 import android.content.IntentFilter;
 import android.os.UserHandle;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.SnapshotCache;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/pm/InstantAppRegistry.java b/services/core/java/com/android/server/pm/InstantAppRegistry.java
index bedc12a..032d030 100644
--- a/services/core/java/com/android/server/pm/InstantAppRegistry.java
+++ b/services/core/java/com/android/server/pm/InstantAppRegistry.java
@@ -46,8 +46,6 @@
 import android.util.PackageUtils;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -55,6 +53,8 @@
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
diff --git a/services/core/java/com/android/server/pm/KeySetManagerService.java b/services/core/java/com/android/server/pm/KeySetManagerService.java
index 7774b6a..f1453c8 100644
--- a/services/core/java/com/android/server/pm/KeySetManagerService.java
+++ b/services/core/java/com/android/server/pm/KeySetManagerService.java
@@ -27,9 +27,9 @@
 import android.util.Base64;
 import android.util.LongSparseArray;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.SharedUserApi;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 218d9d1..cc1d879 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -78,8 +78,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
@@ -89,6 +87,8 @@
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.util.ImageUtils;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemConfig;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 022bf3c..5df73a6 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -29,6 +29,7 @@
 import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
 import static android.content.pm.PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SPLIT;
+import static android.content.pm.PackageManager.INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE;
 import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
 import static android.content.pm.PackageManager.INSTALL_STAGED;
 import static android.content.pm.PackageManager.INSTALL_SUCCEEDED;
@@ -139,8 +140,6 @@
 import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.apk.ApkSignatureVerifier;
 
 import com.android.internal.R;
@@ -156,6 +155,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.dex.DexManager;
@@ -4243,6 +4244,13 @@
     public void requestUserPreapproval(@NonNull PreapprovalDetails details,
             @NonNull IntentSender statusReceiver) {
         validatePreapprovalRequest(details, statusReceiver);
+
+        if (!mPm.isPreapprovalRequestAvailable()) {
+            sendUpdateToRemoteStatusReceiver(INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE,
+                    "Request user pre-approval is currently not available.", null /* extras */);
+            return;
+        }
+
         dispatchPreapprovalRequest();
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 8fed153..dfbe68a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -161,8 +161,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -181,6 +179,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.FunctionalUtils;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.permission.persistence.RuntimePermissionsPersistence;
 import com.android.server.EventLogTags;
 import com.android.server.FgThread;
@@ -495,6 +495,15 @@
     private static final String PROPERTY_KNOWN_DIGESTERS_LIST = "known_digesters_list";
 
     /**
+     * Whether of not requesting the approval before committing sessions is available.
+     *
+     * Flag type: {@code boolean}
+     * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+     */
+    private static final String PROPERTY_IS_PRE_APPROVAL_REQUEST_AVAILABLE =
+            "is_preapproval_available";
+
+    /**
      * The default response for package verification timeout.
      *
      * This can be either PackageManager.VERIFICATION_ALLOW or
@@ -5242,25 +5251,30 @@
                 Map<String, String> classLoaderContextMap,
                 String loaderIsa) {
             int callingUid = Binder.getCallingUid();
-            if (PackageManagerService.PLATFORM_PACKAGE_NAME.equals(loadingPackageName)
-                    && callingUid != Process.SYSTEM_UID) {
+
+            // TODO(b/254043366): System server should not report its own dex load because there's
+            // nothing ART can do with it.
+
+            Computer snapshot = snapshot();
+
+            // System server should be able to report dex load on behalf of other apps. E.g., it
+            // could potentially resend the notifications in order to migrate the existing dex load
+            // info to ART Service.
+            if (!PackageManagerServiceUtils.isSystemOrRoot()
+                    && !snapshot.isCallerSameApp(
+                            loadingPackageName, callingUid, true /* resolveIsolatedUid */)) {
                 Slog.w(PackageManagerService.TAG,
-                        "Non System Server process reporting dex loads as system server. uid="
-                                + callingUid);
-                // Do not record dex loads from processes pretending to be system server.
-                // Only the system server should be assigned the package "android", so reject calls
-                // that don't satisfy the constraint.
-                //
-                // notifyDexLoad is a PM API callable from the app process. So in theory, apps could
-                // craft calls to this API and pretend to be system server. Doing so poses no
-                // particular danger for dex load reporting or later dexopt, however it is a
-                // sensible check to do in order to verify the expectations.
+                        TextUtils.formatSimple(
+                                "Invalid dex load report. loadingPackageName=%s, uid=%d",
+                                loadingPackageName, callingUid));
                 return;
             }
 
+            // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`.
+
             int userId = UserHandle.getCallingUserId();
-            ApplicationInfo ai = snapshot().getApplicationInfo(loadingPackageName, /*flags*/ 0,
-                    userId);
+            ApplicationInfo ai =
+                    snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId);
             if (ai == null) {
                 Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package="
                         + loadingPackageName + ", user=" + userId);
@@ -6889,6 +6903,16 @@
         }
     }
 
+    static boolean isPreapprovalRequestAvailable() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return DeviceConfig.getBoolean(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                    PROPERTY_IS_PRE_APPROVAL_REQUEST_AVAILABLE, true /* defaultValue */);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     /**
      * Returns the array containing per-uid timeout configuration.
      * This is derived from DeviceConfig flags.
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 76858d9..3c1cba3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -316,6 +316,8 @@
                     return runCreateUser();
                 case "remove-user":
                     return runRemoveUser();
+                case "rename-user":
+                    return runRenameUser();
                 case "set-user-restriction":
                     return runSetUserRestriction();
                 case "supports-multiple-users":
@@ -3024,6 +3026,28 @@
         }
     }
 
+    private int runRenameUser() throws RemoteException {
+        String arg = getNextArg();
+        if (arg == null) {
+            getErrPrintWriter().println("Error: no user id specified.");
+            return 1;
+        }
+        int userId = resolveUserId(UserHandle.parseUserArg(arg));
+
+        String name = getNextArg();
+        if (name == null) {
+            Slog.i(TAG, "Resetting name of user " + userId);
+        } else {
+            Slog.i(TAG, "Renaming user " + userId + " to '" + name + "'");
+        }
+
+        IUserManager um = IUserManager.Stub.asInterface(
+                ServiceManager.getService(Context.USER_SERVICE));
+        um.setUserName(userId, name);
+
+        return 0;
+    }
+
     public int runSetUserRestriction() throws RemoteException {
         int userId = UserHandle.USER_SYSTEM;
         String opt = getNextOption();
@@ -3937,6 +3961,11 @@
         return res;
     }
 
+    // Resolves the userId; supports UserHandle.USER_CURRENT, but not other special values
+    private @UserIdInt int resolveUserId(@UserIdInt int userId) {
+        return userId == UserHandle.USER_CURRENT ? ActivityManager.getCurrentUser() : userId;
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -4208,6 +4237,9 @@
         pw.println("        switch or reboot)");
         pw.println("      --wait: Wait until user is removed. Ignored if set-ephemeral-if-in-use");
         pw.println("");
+        pw.println("  rename-user USER_ID [USER_NAME]");
+        pw.println("    Rename USER_ID with USER_NAME (or null when [USER_NAME] is not set)");
+        pw.println("");
         pw.println("  set-user-restriction [--user USER_ID] RESTRICTION VALUE");
         pw.println("");
         pw.println("  get-max-users");
diff --git a/services/core/java/com/android/server/pm/PackageSignatures.java b/services/core/java/com/android/server/pm/PackageSignatures.java
index 76364fe..90f57a7 100644
--- a/services/core/java/com/android/server/pm/PackageSignatures.java
+++ b/services/core/java/com/android/server/pm/PackageSignatures.java
@@ -21,10 +21,10 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.SigningDetails.SignatureSchemeVersion;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/PersistentPreferredActivity.java b/services/core/java/com/android/server/pm/PersistentPreferredActivity.java
index ad3950c..d0ee0c8 100644
--- a/services/core/java/com/android/server/pm/PersistentPreferredActivity.java
+++ b/services/core/java/com/android/server/pm/PersistentPreferredActivity.java
@@ -19,10 +19,10 @@
 import android.content.ComponentName;
 import android.content.IntentFilter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.SnapshotCache;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/pm/PreferredActivity.java b/services/core/java/com/android/server/pm/PreferredActivity.java
index 5bc915f..1a49bf9 100644
--- a/services/core/java/com/android/server/pm/PreferredActivity.java
+++ b/services/core/java/com/android/server/pm/PreferredActivity.java
@@ -19,10 +19,10 @@
 import android.content.ComponentName;
 import android.content.IntentFilter;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.SnapshotCache;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/PreferredActivityHelper.java b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
index 0ca5febd..e7727f0 100644
--- a/services/core/java/com/android/server/pm/PreferredActivityHelper.java
+++ b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
@@ -42,11 +42,11 @@
 import android.util.PrintStreamPrinter;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.net.NetworkPolicyManagerInternal;
 import com.android.server.pm.pkg.PackageStateInternal;
 
diff --git a/services/core/java/com/android/server/pm/PreferredComponent.java b/services/core/java/com/android/server/pm/PreferredComponent.java
index 2a1ca2c..5507e7c 100644
--- a/services/core/java/com/android/server/pm/PreferredComponent.java
+++ b/services/core/java/com/android/server/pm/PreferredComponent.java
@@ -23,10 +23,10 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.pm.pkg.PackageUserState;
 
diff --git a/services/core/java/com/android/server/pm/RestrictionsSet.java b/services/core/java/com/android/server/pm/RestrictionsSet.java
index e5a70c3..e7ad5b9 100644
--- a/services/core/java/com/android/server/pm/RestrictionsSet.java
+++ b/services/core/java/com/android/server/pm/RestrictionsSet.java
@@ -22,10 +22,10 @@
 import android.os.Bundle;
 import android.os.UserManager;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.BundleUtils;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index f2a7651..0eefbfe 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -83,8 +83,6 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
@@ -96,6 +94,8 @@
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.JournaledFile;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.permission.persistence.RuntimePermissionsPersistence;
 import com.android.permission.persistence.RuntimePermissionsState;
 import com.android.server.LocalServices;
diff --git a/services/core/java/com/android/server/pm/SettingsXml.java b/services/core/java/com/android/server/pm/SettingsXml.java
index c53fef7..5fb6731 100644
--- a/services/core/java/com/android/server/pm/SettingsXml.java
+++ b/services/core/java/com/android/server/pm/SettingsXml.java
@@ -19,10 +19,11 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/pm/ShareTargetInfo.java b/services/core/java/com/android/server/pm/ShareTargetInfo.java
index 660874e..bfb5f39 100644
--- a/services/core/java/com/android/server/pm/ShareTargetInfo.java
+++ b/services/core/java/com/android/server/pm/ShareTargetInfo.java
@@ -17,8 +17,9 @@
 
 import android.annotation.NonNull;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index c6a7dd7..0d99075 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -24,11 +24,11 @@
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.ShortcutService.DumpFilter;
 import com.android.server.pm.ShortcutUser.PackageWithUser;
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 890c891..0362ddd 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -53,8 +53,6 @@
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -65,6 +63,8 @@
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.ShortcutService.DumpFilter;
 import com.android.server.pm.ShortcutService.ShortcutOperation;
 import com.android.server.pm.ShortcutService.Stats;
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
index fce6610..79b725d 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageInfo.java
@@ -23,10 +23,10 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningInfo;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.backup.BackupUtils;
 
diff --git a/services/core/java/com/android/server/pm/ShortcutPackageItem.java b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
index 7800183..e20330d 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackageItem.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackageItem.java
@@ -22,11 +22,11 @@
 import android.graphics.Bitmap;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.json.JSONException;
 import org.json.JSONObject;
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 0b20683..b6f09ff 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -100,8 +100,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.IWindowManager;
 
@@ -116,6 +114,8 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.StatLogger;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.pm.ShortcutUser.PackageWithUser;
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index b9fd2fd..20bbf46 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -30,14 +30,14 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.infra.AndroidFuture;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.FgThread;
 import com.android.server.pm.ShortcutService.DumpFilter;
 import com.android.server.pm.ShortcutService.InvalidFileFormatException;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 6577074..4966f94 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -103,8 +103,6 @@
 import android.util.StatsEvent;
 import android.util.TimeUtils;
 import android.util.TypedValue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -119,6 +117,8 @@
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.BundleUtils;
 import com.android.server.LocalServices;
 import com.android.server.LockGuard;
@@ -1540,10 +1540,9 @@
         checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserProperties");
         final UserProperties origProperties = getUserPropertiesInternal(userId);
         if (origProperties != null) {
-            int callingUid = Binder.getCallingUid();
-            boolean exposeAllFields = callingUid == Process.SYSTEM_UID;
-            boolean hasManage = hasPermissionGranted(Manifest.permission.MANAGE_USERS, callingUid);
-            boolean hasQuery = hasPermissionGranted(Manifest.permission.QUERY_USERS, callingUid);
+            boolean exposeAllFields = Binder.getCallingUid() == Process.SYSTEM_UID;
+            boolean hasManage = hasManageUsersPermission();
+            boolean hasQuery = hasQueryUsersPermission();
             return new UserProperties(origProperties, exposeAllFields, hasManage, hasQuery);
         }
         // A non-existent or partial user will reach here.
@@ -2279,26 +2278,31 @@
     @Override
     public void setUserName(@UserIdInt int userId, String name) {
         checkManageUsersPermission("rename users");
-        boolean changed = false;
         synchronized (mPackagesLock) {
             UserData userData = getUserDataNoChecks(userId);
             if (userData == null || userData.info.partial) {
-                Slog.w(LOG_TAG, "setUserName: unknown user #" + userId);
+                Slogf.w(LOG_TAG, "setUserName: unknown user #%d", userId);
                 return;
             }
-            if (name != null && !name.equals(userData.info.name)) {
-                userData.info.name = name;
-                writeUserLP(userData);
-                changed = true;
+            if (Objects.equals(name, userData.info.name)) {
+                Slogf.i(LOG_TAG, "setUserName: ignoring for user #%d as it didn't change (%s)",
+                        userId, getRedacted(name));
+                return;
             }
+            if (name == null) {
+                Slogf.i(LOG_TAG, "setUserName: resetting name of user #%d", userId);
+            } else {
+                Slogf.i(LOG_TAG, "setUserName: setting name of user #%d to %s", userId,
+                        getRedacted(name));
+            }
+            userData.info.name = name;
+            writeUserLP(userData);
         }
-        if (changed) {
-            final long ident = Binder.clearCallingIdentity();
-            try {
-                sendUserInfoChangedBroadcast(userId);
-            } finally {
-                Binder.restoreCallingIdentity(ident);
-            }
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            sendUserInfoChangedBroadcast(userId);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -5519,6 +5523,13 @@
 
     private void removeUserState(final @UserIdInt int userId) {
         Slog.i(LOG_TAG, "Removing user state of user " + userId);
+
+        // Cleanup lock settings.  This must happen before destroyUserKey(), since the user's DE
+        // storage must still be accessible for the lock settings state to be properly cleaned up.
+        mLockPatternUtils.removeUser(userId);
+
+        // Evict and destroy the user's CE and DE encryption keys.  At this point, the user's CE and
+        // DE storage is made inaccessible, except to delete its contents.
         try {
             mContext.getSystemService(StorageManager.class).destroyUserKey(userId);
         } catch (IllegalStateException e) {
@@ -5526,9 +5537,6 @@
             Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e);
         }
 
-        // Cleanup lock settings
-        mLockPatternUtils.removeUser(userId);
-
         // Cleanup package manager settings
         mPm.cleanUpUser(this, userId);
 
@@ -6105,6 +6113,11 @@
         return RESTRICTIONS_FILE_PREFIX + packageName + XML_SUFFIX;
     }
 
+    @Nullable
+    private static String getRedacted(@Nullable String string) {
+        return string == null ? null : string.length() + "_chars";
+    }
+
     @Override
     public void setSeedAccountData(@UserIdInt int userId, String accountName, String accountType,
             PersistableBundle accountOptions, boolean persist) {
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 016c1cb..d1f3341e 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -40,11 +40,11 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.BundleUtils;
 import com.android.server.LocalServices;
 
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 47a3705..415ddd3 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -707,7 +707,7 @@
 
     private List<ComponentName> matchVerifiers(PackageInfoLite pkgInfo,
             List<ResolveInfo> receivers, final PackageVerificationState verificationState) {
-        if (pkgInfo.verifiers.length == 0) {
+        if (pkgInfo.verifiers == null || pkgInfo.verifiers.length == 0) {
             return null;
         }
 
diff --git a/services/core/java/com/android/server/pm/dex/ArtManagerService.java b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
index 37f7ac2..0bdd980 100644
--- a/services/core/java/com/android/server/pm/dex/ArtManagerService.java
+++ b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
@@ -52,6 +52,7 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.Installer;
 import com.android.server.pm.Installer.InstallerException;
+import com.android.server.pm.PackageManagerService;
 import com.android.server.pm.PackageManagerServiceCompilerMapping;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.pkg.AndroidPackage;
@@ -724,6 +725,13 @@
         @Override
         public PackageOptimizationInfo getPackageOptimizationInfo(
                 ApplicationInfo info, String abi, String activityName) {
+            if (info.packageName.equals(PackageManagerService.PLATFORM_PACKAGE_NAME)) {
+                // PackageManagerService.PLATFORM_PACKAGE_NAME in this context means that the
+                // activity is defined in bootclasspath. Currently, we don't have an API to get the
+                // correct optimization info.
+                return PackageOptimizationInfo.createWithNoInfo();
+            }
+
             String compilationReason;
             String compilationFilter;
             try {
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 6b31555..8772de3 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -570,6 +570,7 @@
             ai.metaData = null;
         }
         ai.applicationInfo = applicationInfo;
+        ai.targetDisplayCategory = a.getTargetDisplayCategory();
         ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts());
         assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, userId);
         return ai;
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 37abeac..8588267 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -62,12 +62,12 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.R;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
 import com.android.server.pm.KnownPackages;
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermission.java b/services/core/java/com/android/server/pm/permission/LegacyPermission.java
index 5f8f342..d8b4faa 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermission.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermission.java
@@ -21,9 +21,9 @@
 import android.annotation.Nullable;
 import android.content.pm.PermissionInfo;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.DumpState;
 import com.android.server.pm.PackageManagerService;
 
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java
index f63600a..fc6d202 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionSettings.java
@@ -21,11 +21,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.DumpState;
 import com.android.server.pm.PackageManagerService;
 
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index c81a3ee..799ef41 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -649,8 +649,8 @@
             Permission bp = mRegistry.getPermission(info.name);
             added = bp == null;
             int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel);
+            enforcePermissionCapLocked(info, tree);
             if (added) {
-                enforcePermissionCapLocked(info, tree);
                 bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC);
             } else if (!bp.isDynamic()) {
                 throw new SecurityException("Not allowed to modify non-dynamic permission "
@@ -2156,6 +2156,46 @@
     }
 
     /**
+     * If the package was below api 23, got the SYSTEM_ALERT_WINDOW permission automatically, and
+     * then updated past api 23, and the app does not satisfy any of the other SAW permission flags,
+     * the permission should be revoked.
+     *
+     * @param newPackage The new package that was installed
+     * @param oldPackage The old package that was updated
+     */
+    private void revokeSystemAlertWindowIfUpgradedPast23(
+            @NonNull AndroidPackage newPackage,
+            @NonNull AndroidPackage oldPackage) {
+        if (oldPackage.getTargetSdkVersion() >= Build.VERSION_CODES.M
+                || newPackage.getTargetSdkVersion() < Build.VERSION_CODES.M
+                || !newPackage.getRequestedPermissions()
+                .contains(Manifest.permission.SYSTEM_ALERT_WINDOW)) {
+            return;
+        }
+
+        Permission saw;
+        synchronized (mLock) {
+            saw = mRegistry.getPermission(Manifest.permission.SYSTEM_ALERT_WINDOW);
+        }
+        final PackageStateInternal ps =
+                mPackageManagerInt.getPackageStateInternal(newPackage.getPackageName());
+        if (shouldGrantPermissionByProtectionFlags(newPackage, ps, saw, new ArraySet<>())
+                || shouldGrantPermissionBySignature(newPackage, saw)) {
+            return;
+        }
+        for (int userId : getAllUserIds()) {
+            try {
+                revokePermissionFromPackageForUser(newPackage.getPackageName(),
+                        Manifest.permission.SYSTEM_ALERT_WINDOW, false, userId,
+                        mDefaultPermissionCallback);
+            } catch (IllegalStateException | SecurityException e) {
+                Log.e(TAG, "unable to revoke SYSTEM_ALERT_WINDOW for "
+                        + newPackage.getPackageName() + " user " + userId, e);
+            }
+        }
+    }
+
+    /**
      * We might auto-grant permissions if any permission of the group is already granted. Hence if
      * the group of a granted permission changes we need to revoke it to avoid having permissions of
      * the new group auto-granted.
@@ -4691,6 +4731,7 @@
                 if (hasOldPkg) {
                     revokeRuntimePermissionsIfGroupChangedInternal(pkg, oldPkg);
                     revokeStoragePermissionsIfScopeExpandedInternal(pkg, oldPkg);
+                    revokeSystemAlertWindowIfUpgradedPast23(pkg, oldPkg);
                 }
                 if (hasPermissionDefinitionChanges) {
                     revokeRuntimePermissionsIfPermissionDefinitionChangedInternal(
diff --git a/services/core/java/com/android/server/pm/pkg/SuspendParams.java b/services/core/java/com/android/server/pm/pkg/SuspendParams.java
index 0926ba2..dc48a33 100644
--- a/services/core/java/com/android/server/pm/pkg/SuspendParams.java
+++ b/services/core/java/com/android/server/pm/pkg/SuspendParams.java
@@ -21,8 +21,9 @@
 import android.os.BaseBundle;
 import android.os.PersistableBundle;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java
index 0320818..e019215 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivity.java
@@ -96,4 +96,10 @@
     ActivityInfo.WindowLayout getWindowLayout();
 
     boolean isSupportsSizeChanges();
+
+    /**
+     * Gets the category of the target display this activity is supposed to run on.
+     */
+    @Nullable
+    String getTargetDisplayCategory();
 }
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java
index aebe133..278e547 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java
@@ -96,6 +96,9 @@
     @Nullable
     private ActivityInfo.WindowLayout windowLayout;
 
+    @Nullable
+    private String mTargetDisplayCategory;
+
     public ParsedActivityImpl(ParsedActivityImpl other) {
         super(other);
         this.theme = other.theme;
@@ -122,6 +125,7 @@
         this.colorMode = other.colorMode;
         this.windowLayout = other.windowLayout;
         this.mKnownActivityEmbeddingCerts = other.mKnownActivityEmbeddingCerts;
+        this.mTargetDisplayCategory = other.mTargetDisplayCategory;
     }
 
     /**
@@ -189,6 +193,7 @@
         alias.requestedVrComponent = target.getRequestedVrComponent();
         alias.setDirectBootAware(target.isDirectBootAware());
         alias.setProcessName(target.getProcessName());
+        alias.setTargetDisplayCategory(target.getTargetDisplayCategory());
         return alias;
 
         // Not all attributes from the target ParsedActivity are copied to the alias.
@@ -316,6 +321,7 @@
             dest.writeBoolean(false);
         }
         sForStringSet.parcel(this.mKnownActivityEmbeddingCerts, dest, flags);
+        dest.writeString8(this.mTargetDisplayCategory);
     }
 
     public ParsedActivityImpl() {
@@ -350,6 +356,7 @@
             windowLayout = new ActivityInfo.WindowLayout(in);
         }
         this.mKnownActivityEmbeddingCerts = sForStringSet.unparcel(in);
+        this.mTargetDisplayCategory = in.readString8();
     }
 
     @NonNull
@@ -406,7 +413,8 @@
             @Nullable String requestedVrComponent,
             int rotationAnimation,
             int colorMode,
-            @Nullable ActivityInfo.WindowLayout windowLayout) {
+            @Nullable ActivityInfo.WindowLayout windowLayout,
+            @Nullable String targetDisplayCategory) {
         this.theme = theme;
         this.uiOptions = uiOptions;
         this.targetActivity = targetActivity;
@@ -431,6 +439,7 @@
         this.rotationAnimation = rotationAnimation;
         this.colorMode = colorMode;
         this.windowLayout = windowLayout;
+        this.mTargetDisplayCategory = targetDisplayCategory;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -551,6 +560,11 @@
     }
 
     @DataClass.Generated.Member
+    public @Nullable String getTargetDisplayCategory() {
+        return mTargetDisplayCategory;
+    }
+
+    @DataClass.Generated.Member
     public @NonNull ParsedActivityImpl setTheme( int value) {
         theme = value;
         return this;
@@ -676,11 +690,17 @@
         return this;
     }
 
+    @DataClass.Generated.Member
+    public @NonNull ParsedActivityImpl setTargetDisplayCategory(@NonNull String value) {
+        mTargetDisplayCategory = value;
+        return this;
+    }
+
     @DataClass.Generated(
-            time = 1644372875433L,
+            time = 1664805688714L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/component/ParsedActivityImpl.java",
-            inputSignatures = "private  int theme\nprivate  int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate  int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate  int launchMode\nprivate  int documentLaunchMode\nprivate  int maxRecents\nprivate  int configChanges\nprivate  int softInputMode\nprivate  int persistableMode\nprivate  int lockTaskLaunchMode\nprivate  int screenOrientation\nprivate  int resizeMode\nprivate  float maxAspectRatio\nprivate  float minAspectRatio\nprivate  boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate  int rotationAnimation\nprivate  int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.server.pm.pkg.component.ParsedActivityImpl> CREATOR\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.server.pm.pkg.component.ParsedActivity)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic  void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic  java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.server.pm.pkg.component.ParsedMainComponentImpl implements [com.android.server.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
+            inputSignatures = "private  int theme\nprivate  int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate  int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate  int launchMode\nprivate  int documentLaunchMode\nprivate  int maxRecents\nprivate  int configChanges\nprivate  int softInputMode\nprivate  int persistableMode\nprivate  int lockTaskLaunchMode\nprivate  int screenOrientation\nprivate  int resizeMode\nprivate  float maxAspectRatio\nprivate  float minAspectRatio\nprivate  boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate  int rotationAnimation\nprivate  int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\nprivate @android.annotation.Nullable java.lang.String mTargetDisplayCategory\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.server.pm.pkg.component.ParsedActivityImpl> CREATOR\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.server.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.server.pm.pkg.component.ParsedActivity)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic  com.android.server.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic  void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic  java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.server.pm.pkg.component.ParsedMainComponentImpl implements [com.android.server.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
index bbbf598..305062b 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java
@@ -29,6 +29,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
+import android.content.pm.parsing.FrameworkParsingPackageUtils;
 import android.content.pm.parsing.result.ParseInput;
 import android.content.pm.parsing.result.ParseInput.DeferredError;
 import android.content.pm.parsing.result.ParseResult;
@@ -219,6 +220,18 @@
                 pkg.setVisibleToInstantApps(true);
             }
 
+            String targetDisplayCategory = sa.getNonConfigurationString(
+                    R.styleable.AndroidManifestActivity_targetDisplayCategory, 0);
+
+            if (targetDisplayCategory != null
+                    && FrameworkParsingPackageUtils.validateName(targetDisplayCategory,
+                    false /* requireSeparator */, false /* requireFilename */) != null) {
+                return input.error("targetDisplayCategory attribute can only consists of "
+                        + "alphanumeric characters, '_', and '.'");
+            }
+
+            activity.setTargetDisplayCategory(targetDisplayCategory);
+
             return parseActivityOrAlias(activity, pkg, tag, parser, res, sa, receiver,
                     false /*isAlias*/, visibleToEphemeral, input,
                     R.styleable.AndroidManifestActivity_parentActivityName,
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java
index 4bad102..9fb8297 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationLegacySettings.java
@@ -23,10 +23,10 @@
 import android.content.pm.PackageManager;
 import android.util.ArrayMap;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.SettingsXml;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
index 1714086..53ee189 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerInternal.java
@@ -33,9 +33,9 @@
 import android.os.UserHandle;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Computer;
 import com.android.server.pm.PackageSetting;
 import com.android.server.pm.Settings;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
index e803457..ac6d795 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
@@ -27,9 +27,9 @@
 import android.util.ArraySet;
 import android.util.PackageUtils;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.SettingsXml;
 import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState;
 import com.android.server.pm.verify.domain.models.DomainVerificationPkgState;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index 400af36..595c34c 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -46,11 +46,11 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.CollectionUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.SystemConfig;
 import com.android.server.SystemService;
 import com.android.server.compat.PlatformCompat;
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java
index cde72cd..d256830 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationSettings.java
@@ -23,11 +23,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Computer;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageStateInternal;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d39b649..a6fac4d 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -114,6 +114,7 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Rect;
+import android.hardware.SensorPrivacyManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.hdmi.HdmiAudioSystemClient;
@@ -391,6 +392,7 @@
     IStatusBarService mStatusBarService;
     StatusBarManagerInternal mStatusBarManagerInternal;
     AudioManagerInternal mAudioManagerInternal;
+    SensorPrivacyManager mSensorPrivacyManager;
     DisplayManager mDisplayManager;
     DisplayManagerInternal mDisplayManagerInternal;
     boolean mPreloadedRecentApps;
@@ -1912,6 +1914,7 @@
         mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class);
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+        mSensorPrivacyManager = mContext.getSystemService(SensorPrivacyManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mPackageManager = mContext.getPackageManager();
@@ -3079,6 +3082,18 @@
         return key_not_consumed;
     }
 
+    private void toggleMicrophoneMuteFromKey() {
+        if (mSensorPrivacyManager.supportsSensorToggle(
+                SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
+                SensorPrivacyManager.Sensors.MICROPHONE)) {
+            boolean isEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
+                    SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
+                    SensorPrivacyManager.Sensors.MICROPHONE);
+            mSensorPrivacyManager.setSensorPrivacy(SensorPrivacyManager.Sensors.MICROPHONE,
+                    !isEnabled);
+        }
+    }
+
     /**
      * TV only: recognizes a remote control gesture for capturing a bug report.
      */
@@ -4011,11 +4026,16 @@
                 break;
             }
 
+            case KeyEvent.KEYCODE_MUTE:
+                result &= ~ACTION_PASS_TO_USER;
+                if (down && event.getRepeatCount() == 0) {
+                    toggleMicrophoneMuteFromKey();
+                }
+                break;
             case KeyEvent.KEYCODE_MEDIA_PLAY:
             case KeyEvent.KEYCODE_MEDIA_PAUSE:
             case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
             case KeyEvent.KEYCODE_HEADSETHOOK:
-            case KeyEvent.KEYCODE_MUTE:
             case KeyEvent.KEYCODE_MEDIA_STOP:
             case KeyEvent.KEYCODE_MEDIA_NEXT:
             case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
@@ -4193,7 +4213,9 @@
         if (mRequestedOrSleepingDefaultDisplay) {
             mCameraGestureTriggeredDuringGoingToSleep = true;
             // Wake device up early to prevent display doing redundant turning off/on stuff.
-            wakeUpFromPowerKey(event.getDownTime());
+            wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
+                    PowerManager.WAKE_REASON_CAMERA_LAUNCH,
+                    "android.policy:CAMERA_GESTURE_PREVENT_LOCK");
         }
         return true;
     }
@@ -4726,11 +4748,6 @@
             }
             mDefaultDisplayRotation.updateOrientationListener();
             reportScreenStateToVrManager(false);
-            if (mCameraGestureTriggeredDuringGoingToSleep) {
-                wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
-                        PowerManager.WAKE_REASON_CAMERA_LAUNCH,
-                        "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 69fb22c..1fe82f4 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -22,8 +22,8 @@
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.trust.TrustManager;
-import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.hardware.display.DisplayManagerInternal;
@@ -796,18 +796,19 @@
         }
 
         if (mActivityManagerInternal.isSystemReady()) {
-            mContext.sendOrderedBroadcastAsUser(mScreenOnIntent, UserHandle.ALL, null,
-                    AppOpsManager.OP_NONE, mScreenOnOptions, mWakeUpBroadcastDone, mHandler,
-                    0, null, null);
+            final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
+            mActivityManagerInternal.broadcastIntent(mScreenOnIntent, mWakeUpBroadcastDone,
+                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOptions);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 2, 1);
             sendNextBroadcast();
         }
     }
 
-    private final BroadcastReceiver mWakeUpBroadcastDone = new BroadcastReceiver() {
+    private final IIntentReceiver mWakeUpBroadcastDone = new IIntentReceiver.Stub() {
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
+                boolean ordered, boolean sticky, int sendingUser) {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_DONE, 1,
                     SystemClock.uptimeMillis() - mBroadcastStartTime, 1);
             sendNextBroadcast();
@@ -820,18 +821,19 @@
         }
 
         if (mActivityManagerInternal.isSystemReady()) {
-            mContext.sendOrderedBroadcastAsUser(mScreenOffIntent, UserHandle.ALL, null,
-                    AppOpsManager.OP_NONE, mScreenOffOptions, mGoToSleepBroadcastDone, mHandler,
-                    0, null, null);
+            final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
+            mActivityManagerInternal.broadcastIntent(mScreenOffIntent, mGoToSleepBroadcastDone,
+                    null, ordered, UserHandle.USER_ALL, null, null, mScreenOffOptions);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 3, 1);
             sendNextBroadcast();
         }
     }
 
-    private final BroadcastReceiver mGoToSleepBroadcastDone = new BroadcastReceiver() {
+    private final IIntentReceiver mGoToSleepBroadcastDone = new IIntentReceiver.Stub() {
         @Override
-        public void onReceive(Context context, Intent intent) {
+        public void performReceive(Intent intent, int resultCode, String data, Bundle extras,
+                boolean ordered, boolean sticky, int sendingUser) {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_DONE, 0,
                     SystemClock.uptimeMillis() - mBroadcastStartTime, 1);
             sendNextBroadcast();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 4784723..d8b1120 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -42,8 +42,6 @@
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.SynchronousUserSwitchObserver;
-import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -64,7 +62,6 @@
 import android.os.BatteryManagerInternal;
 import android.os.BatterySaverPolicyConfig;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IBinder;
@@ -127,7 +124,6 @@
 import com.android.server.UserspaceRebootLogger;
 import com.android.server.Watchdog;
 import com.android.server.am.BatteryStatsService;
-import com.android.server.compat.PlatformCompat;
 import com.android.server.lights.LightsManager;
 import com.android.server.lights.LogicalLight;
 import com.android.server.policy.WindowManagerPolicy;
@@ -284,17 +280,6 @@
      */
     private static final long ENHANCED_DISCHARGE_PREDICTION_BROADCAST_MIN_DELAY_MS = 60 * 1000L;
 
-    /**
-     * Apps targeting Android U and above need to define
-     * {@link android.Manifest.permission#TURN_SCREEN_ON} in their manifest for
-     * {@link android.os.PowerManager#ACQUIRE_CAUSES_WAKEUP} to have any effect.
-     * Note that most applications should use {@link android.R.attr#turnScreenOn} or
-     * {@link android.app.Activity#setTurnScreenOn(boolean)} instead, as this prevents the
-     * previous foreground app from being resumed first when the screen turns on.
-     */
-    @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public static final long REQUIRE_TURN_SCREEN_ON_PERMISSION = 216114297L;
     /** Reason ID for holding display suspend blocker. */
     private static final String HOLDING_DISPLAY_SUSPEND_BLOCKER = "holding display";
 
@@ -318,7 +303,6 @@
     private final SystemPropertiesWrapper mSystemProperties;
     private final Clock mClock;
     private final Injector mInjector;
-    private final PlatformCompat mPlatformCompat;
 
     private AppOpsManager mAppOpsManager;
     private LightsManager mLightsManager;
@@ -1012,11 +996,6 @@
                 public void set(String key, String val) {
                     SystemProperties.set(key, val);
                 }
-
-                @Override
-                public boolean getBoolean(String key, boolean def) {
-                    return SystemProperties.getBoolean(key, def);
-                }
             };
         }
 
@@ -1053,10 +1032,6 @@
         AppOpsManager createAppOpsManager(Context context) {
             return context.getSystemService(AppOpsManager.class);
         }
-
-        PlatformCompat createPlatformCompat(Context context) {
-            return context.getSystemService(PlatformCompat.class);
-        }
     }
 
     final Constants mConstants;
@@ -1114,8 +1089,6 @@
 
         mAppOpsManager = injector.createAppOpsManager(mContext);
 
-        mPlatformCompat = injector.createPlatformCompat(mContext);
-
         mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener();
 
         // Save brightness values:
@@ -1626,28 +1599,14 @@
         }
         if (mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON, opUid, opPackageName)
                 == AppOpsManager.MODE_ALLOWED) {
-            if (mPlatformCompat.isChangeEnabledByPackageName(REQUIRE_TURN_SCREEN_ON_PERMISSION,
-                    opPackageName, UserHandle.getUserId(opUid))) {
-                if (mContext.checkCallingOrSelfPermission(
-                        android.Manifest.permission.TURN_SCREEN_ON)
-                        == PackageManager.PERMISSION_GRANTED) {
-                    if (DEBUG_SPEW) {
-                        Slog.d(TAG, "Allowing device wake-up from app " + opPackageName);
-                    }
-                    return true;
-                }
-            } else {
-                // android.permission.TURN_SCREEN_ON has only been introduced in Android U, only
-                // check for appOp for apps targeting lower SDK versions
-                if (DEBUG_SPEW) {
-                    Slog.d(TAG, "Allowing device wake-up from app with "
-                            + "REQUIRE_TURN_SCREEN_ON_PERMISSION disabled " + opPackageName);
-                }
+            if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.TURN_SCREEN_ON)
+                    == PackageManager.PERMISSION_GRANTED) {
+                Slog.i(TAG, "Allowing device wake-up from app " + opPackageName);
                 return true;
             }
         }
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
-            Slog.d(TAG, "Device wake-up will be denied without android.permission.TURN_SCREEN_ON");
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
+            Slog.d(TAG, "Device wake-up allowed by debug.power.permissionless_turn_screen_on");
             return true;
         }
         Slog.w(TAG, "Not allowing device wake-up for " + opPackageName);
diff --git a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
index c68f9c6..1acf798 100644
--- a/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
+++ b/services/core/java/com/android/server/power/SystemPropertiesWrapper.java
@@ -48,19 +48,4 @@
      * SELinux. libc will log the underlying reason.
      */
     void set(@NonNull String key, @Nullable String val);
-
-    /**
-     * Get the value for the given {@code key}, returned as a boolean.
-     * Values 'n', 'no', '0', 'false' or 'off' are considered false.
-     * Values 'y', 'yes', '1', 'true' or 'on' are considered true.
-     * (case sensitive).
-     * If the key does not exist, or has any other value, then the default
-     * result is returned.
-     *
-     * @param key the key to lookup
-     * @param def a default value to return
-     * @return the key parsed as a boolean, or def if the key isn't found or is
-     *         not able to be parsed as a boolean.
-     */
-    boolean getBoolean(@NonNull String key, boolean def);
 }
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 1e5b498..916df89 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -99,8 +99,6 @@
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -130,6 +128,8 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.NetworkCapabilitiesUtils;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
index 50cb33c..0d7a140 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsStore.java
@@ -25,11 +25,11 @@
 import android.util.Log;
 import android.util.LongArray;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java b/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
index f797f09..58b2443 100644
--- a/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
+++ b/services/core/java/com/android/server/sensorprivacy/AllSensorStateController.java
@@ -21,13 +21,13 @@
 import android.os.Handler;
 import android.util.AtomicFile;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/sensorprivacy/PersistedState.java b/services/core/java/com/android/server/sensorprivacy/PersistedState.java
index e79efdb8..85ec101 100644
--- a/services/core/java/com/android/server/sensorprivacy/PersistedState.java
+++ b/services/core/java/com/android/server/sensorprivacy/PersistedState.java
@@ -28,14 +28,14 @@
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
 import com.android.internal.util.function.QuadConsumer;
 import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index c79bc89..61c21e6 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -26,6 +26,7 @@
 import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
 import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
 import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
+import static android.app.AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -81,6 +82,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
+import android.database.ContentObserver;
 import android.graphics.drawable.Icon;
 import android.hardware.ISensorPrivacyListener;
 import android.hardware.ISensorPrivacyManager;
@@ -199,6 +201,7 @@
         if (phase == PHASE_SYSTEM_SERVICES_READY) {
             mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
             mCallStateHelper = new CallStateHelper();
+            mSensorPrivacyServiceImpl.registerSettingsObserver();
         } else if (phase == PHASE_ACTIVITY_MANAGER_READY) {
             mCameraPrivacyLightController = new CameraPrivacyLightController(mContext);
         }
@@ -271,7 +274,7 @@
             mSensorPrivacyStateController = SensorPrivacyStateController.getInstance();
 
             int[] micAndCameraOps = new int[]{OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE,
-                    OP_CAMERA, OP_PHONE_CALL_CAMERA};
+                    OP_CAMERA, OP_PHONE_CALL_CAMERA, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO};
             mAppOpsManager.startWatchingNoted(micAndCameraOps, this);
             mAppOpsManager.startWatchingStarted(micAndCameraOps, this);
 
@@ -340,7 +343,8 @@
 
             int sensor;
             if (result == MODE_IGNORED) {
-                if (code == OP_RECORD_AUDIO || code == OP_PHONE_CALL_MICROPHONE) {
+                if (code == OP_RECORD_AUDIO || code == OP_PHONE_CALL_MICROPHONE
+                        || code == OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO) {
                     sensor = MICROPHONE;
                 } else if (code == OP_CAMERA || code == OP_PHONE_CALL_CAMERA) {
                     sensor = CAMERA;
@@ -1072,6 +1076,14 @@
                     // restrict it when the microphone is disabled
                     mAppOpsManagerInternal.setGlobalRestriction(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
                             enabled, mAppOpsRestrictionToken);
+
+                    // Set restriction for OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO
+                    boolean allowed = (Settings.Global.getInt(mContext.getContentResolver(),
+                            Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED, 1)
+                            == 1);
+                    mAppOpsManagerInternal.setGlobalRestriction(
+                            OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, enabled && !allowed,
+                            mAppOpsRestrictionToken);
                     break;
                 case CAMERA:
                     mAppOpsManagerInternal.setGlobalRestriction(OP_CAMERA, enabled,
@@ -1112,6 +1124,19 @@
             }
         }
 
+        private void registerSettingsObserver() {
+            mContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(
+                            Settings.Global.RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO_ENABLED),
+                    false, new ContentObserver(mHandler) {
+                        @Override
+                        public void onChange(boolean selfChange) {
+                            setGlobalRestriction(MICROPHONE,
+                                    isCombinedToggleSensorPrivacyEnabled(MICROPHONE));
+                        }
+                    });
+        }
+
         /**
          * A owner of a suppressor token died. Clean up.
          *
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7ccf85f..d378b11 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -2272,6 +2272,25 @@
 
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        boolean proto = false;
+        for (int i = 0; i < args.length; i++) {
+            if ("--proto".equals(args[i])) {
+                proto = true;
+            }
+        }
+        if (proto) {
+            if (mBar == null)  return;
+            try (TransferPipe tp = new TransferPipe()) {
+                // Sending the command to the remote, which needs to execute async to avoid blocking
+                // See Binder#dumpAsync() for inspiration
+                mBar.dumpProto(args, tp.getWriteFd());
+                // Times out after 5s
+                tp.go(fd);
+            } catch (Throwable t) {
+                Slog.e(TAG, "Error sending command to IStatusBar", t);
+            }
+            return;
+        }
 
         synchronized (mLock) {
             for (int i = 0; i < mDisplayUiState.size(); i++) {
diff --git a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
index fc77ef1..dad3a78 100644
--- a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
+++ b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java
@@ -47,11 +47,11 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseLongArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.Installer;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
index ff0529f..8a6f927 100644
--- a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
+++ b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
@@ -16,10 +16,13 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.infoLog;
 
 import android.annotation.NonNull;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 
 import com.android.i18n.timezone.ZoneInfoDb;
 
@@ -53,7 +56,12 @@
         // enables immediate failover to a secondary provider, one that might provide valid IDs for
         // the same location, which should provide better behavior than just ignoring the event.
         if (hasInvalidZones(event)) {
-            return TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis());
+            TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder(
+                    event.getTimeZoneProviderStatus())
+                    .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED)
+                    .build();
+            return TimeZoneProviderEvent.createUncertainEvent(
+                    event.getCreationElapsedMillis(), providerStatus);
         }
 
         return event;
diff --git a/services/core/java/com/android/server/tv/PersistentDataStore.java b/services/core/java/com/android/server/tv/PersistentDataStore.java
index 72556a7..f8a9988 100644
--- a/services/core/java/com/android/server/tv/PersistentDataStore.java
+++ b/services/core/java/com/android/server/tv/PersistentDataStore.java
@@ -26,11 +26,11 @@
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java b/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
index 7f49eea..39df450 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
@@ -20,10 +20,10 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index 6aa06e8..01fdc88 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -71,14 +71,14 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.IoThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 8ac4fd4..141be70 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -65,6 +65,9 @@
     public final DeviceVibrationEffectAdapter deviceEffectAdapter;
     public final VibrationThread.VibratorManagerHooks vibratorManagerHooks;
 
+    // Not guarded by lock because they're not modified by this conductor, it's used here only to
+    // check immutable attributes. The status and other mutable states are changed by the service or
+    // by the vibrator steps.
     private final Vibration mVibration;
     private final SparseArray<VibratorController> mVibrators = new SparseArray<>();
 
@@ -412,6 +415,16 @@
         }
     }
 
+    /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */
+    public boolean wasNotifiedToCancel() {
+        if (Build.IS_DEBUGGABLE) {
+            expectIsVibrationThread(false);
+        }
+        synchronized (mLock) {
+            return mSignalCancel != null;
+        }
+    }
+
     @GuardedBy("mLock")
     private boolean hasPendingNotifySignalLocked() {
         if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 8514e27..8613b50 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -864,8 +864,8 @@
         }
 
         Vibration currentVibration = mCurrentVibration.getVibration();
-        if (currentVibration.hasEnded()) {
-            // Current vibration is finishing up, it should not block incoming vibrations.
+        if (currentVibration.hasEnded() || mCurrentVibration.wasNotifiedToCancel()) {
+            // Current vibration has ended or is cancelling, should not block incoming vibrations.
             return null;
         }
 
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 5f420bf..abb57bc 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -99,8 +99,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 import android.view.DisplayInfo;
@@ -111,6 +109,8 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.JournaledFile;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.EventLogTags;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2232aa1..17a9a63 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -312,8 +312,6 @@
 import android.util.MergedConfiguration;
 import android.util.Slog;
 import android.util.TimeUtils;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 import android.view.AppTransitionAnimationSpec;
 import android.view.DisplayInfo;
@@ -349,6 +347,8 @@
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.am.AppTimeTracker;
 import com.android.server.am.PendingIntentRecord;
@@ -8454,7 +8454,7 @@
         getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                 mCompatDisplayInsets);
         // Use current screen layout as source because the size of app is independent to parent.
-        resolvedConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
+        resolvedConfig.screenLayout = computeScreenLayout(
                 getConfiguration().screenLayout, resolvedConfig.screenWidthDp,
                 resolvedConfig.screenHeightDp);
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 416d546..ecc43f7 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -460,7 +460,6 @@
     KeyguardController mKeyguardController;
     private final ClientLifecycleManager mLifecycleManager;
 
-    @Nullable
     final BackNavigationController mBackNavigationController;
 
     private TaskChangeNotificationController mTaskChangeNotificationController;
@@ -847,8 +846,7 @@
         mTaskOrganizerController = mWindowOrganizerController.mTaskOrganizerController;
         mTaskFragmentOrganizerController =
                 mWindowOrganizerController.mTaskFragmentOrganizerController;
-        mBackNavigationController = BackNavigationController.isEnabled()
-                ? new BackNavigationController() : null;
+        mBackNavigationController = new BackNavigationController();
     }
 
     public void onSystemReady() {
@@ -1031,9 +1029,7 @@
             mLockTaskController.setWindowManager(wm);
             mTaskSupervisor.setWindowManager(wm);
             mRootWindowContainer.setWindowManager(wm);
-            if (mBackNavigationController != null) {
-                mBackNavigationController.setWindowManager(wm);
-            }
+            mBackNavigationController.setWindowManager(wm);
         }
     }
 
@@ -1852,9 +1848,6 @@
             IWindowFocusObserver observer, BackAnimationAdapter adapter) {
         mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,
                 "startBackNavigation()");
-        if (mBackNavigationController == null) {
-            return null;
-        }
 
         return mBackNavigationController.startBackNavigation(observer, adapter);
     }
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index 5a24099..d22c38e 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -31,10 +31,11 @@
 import android.util.AtomicFile;
 import android.util.DisplayMetrics;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e977447..1cb83f12 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -65,12 +65,12 @@
     // TODO (b/241808055) Find a appropriate time to remove during refactor
     // Execute back animation with legacy transition system. Temporary flag for easier debugging.
     static final boolean ENABLE_SHELL_TRANSITIONS = WindowManagerService.sEnableShellTransitions;
+
     /**
-     * Returns true if the back predictability feature is enabled
+     * true if the back predictability feature is enabled
      */
-    static boolean isEnabled() {
-        return SystemProperties.getInt("persist.wm.debug.predictive_back", 1) != 0;
-    }
+    static final boolean sPredictBackEnable =
+            SystemProperties.getBoolean("persist.wm.debug.predictive_back", true);
 
     static boolean isScreenshotEnabled() {
         return SystemProperties.getInt("persist.wm.debug.predictive_back_screenshot", 0) != 0;
@@ -88,6 +88,9 @@
     @Nullable
     BackNavigationInfo startBackNavigation(
             IWindowFocusObserver observer, BackAnimationAdapter adapter) {
+        if (!sPredictBackEnable) {
+            return null;
+        }
         final WindowManagerService wmService = mWindowManagerService;
         mFocusObserver = observer;
 
@@ -588,6 +591,7 @@
 
         ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
                 "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
+        activity.mTaskSupervisor.mStoppingActivities.remove(activity);
         activity.getDisplayContent().ensureActivitiesVisible(null /* starting */,
                 0 /* configChanges */, false /* preserveWindows */, true);
     }
diff --git a/services/core/java/com/android/server/wm/CompatModePackages.java b/services/core/java/com/android/server/wm/CompatModePackages.java
index 6f19450..a035948 100644
--- a/services/core/java/com/android/server/wm/CompatModePackages.java
+++ b/services/core/java/com/android/server/wm/CompatModePackages.java
@@ -44,11 +44,11 @@
 import android.util.DisplayMetrics;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.protolog.common.ProtoLog;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 3c847ce..739f41f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2188,8 +2188,7 @@
             mDisplayInfo.flags &= ~Display.FLAG_SCALING_DISABLED;
         }
 
-        computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, dw, dh,
-                mDisplayMetrics.density, outConfig);
+        computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
 
         mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
                 mDisplayInfo);
@@ -2289,8 +2288,7 @@
         displayInfo.appHeight = appBounds.height();
         final DisplayCutout displayCutout = calculateDisplayCutoutForRotation(rotation);
         displayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout;
-        computeSizeRangesAndScreenLayout(displayInfo, rotated, dw, dh,
-                mDisplayMetrics.density, outConfig);
+        computeSizeRanges(displayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
         return displayInfo;
     }
 
@@ -2309,6 +2307,9 @@
         outConfig.screenHeightDp = (int) (info.mConfigFrame.height() / density + 0.5f);
         outConfig.compatScreenWidthDp = (int) (outConfig.screenWidthDp / mCompatibleScreenScale);
         outConfig.compatScreenHeightDp = (int) (outConfig.screenHeightDp / mCompatibleScreenScale);
+        outConfig.screenLayout = computeScreenLayout(
+                Configuration.resetScreenLayout(outConfig.screenLayout),
+                outConfig.screenWidthDp, outConfig.screenHeightDp);
 
         final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
         outConfig.compatSmallestScreenWidthDp = computeCompatSmallestWidth(rotated, dw, dh);
@@ -2450,7 +2451,7 @@
         return curSize;
     }
 
-    private void computeSizeRangesAndScreenLayout(DisplayInfo displayInfo, boolean rotated,
+    private void computeSizeRanges(DisplayInfo displayInfo, boolean rotated,
             int dw, int dh, float density, Configuration outConfig) {
 
         // We need to determine the smallest width that will occur under normal
@@ -2477,31 +2478,8 @@
         if (outConfig == null) {
             return;
         }
-        int sl = Configuration.resetScreenLayout(outConfig.screenLayout);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_0, density, unrotDw, unrotDh);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_90, density, unrotDh, unrotDw);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_180, density, unrotDw, unrotDh);
-        sl = reduceConfigLayout(sl, Surface.ROTATION_270, density, unrotDh, unrotDw);
         outConfig.smallestScreenWidthDp =
                 (int) (displayInfo.smallestNominalAppWidth / density + 0.5f);
-        outConfig.screenLayout = sl;
-    }
-
-    private int reduceConfigLayout(int curLayout, int rotation, float density, int dw, int dh) {
-        // Get the app screen size at this rotation.
-        final Rect size = mDisplayPolicy.getDecorInsetsInfo(rotation, dw, dh).mNonDecorFrame;
-
-        // Compute the screen layout size class for this rotation.
-        int longSize = size.width();
-        int shortSize = size.height();
-        if (longSize < shortSize) {
-            int tmp = longSize;
-            longSize = shortSize;
-            shortSize = tmp;
-        }
-        longSize = (int) (longSize / density + 0.5f);
-        shortSize = (int) (shortSize / density + 0.5f);
-        return Configuration.reduceScreenLayout(curLayout, longSize, shortSize);
     }
 
     private void adjustDisplaySizeRanges(DisplayInfo displayInfo, int rotation, int dw, int dh) {
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 4a70fa3..1abb0a1 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -30,14 +30,14 @@
 import android.os.Environment;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.DisplayAddress;
 import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index bf4b65d..3a8fbbb 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -173,6 +173,7 @@
         mWindowContainer = windowContainer;
         // TODO: remove the frame provider for non-WindowState container.
         mFrameProvider = frameProvider;
+        mOverrideFrames.clear();
         mOverrideFrameProviders = overrideFrameProviders;
         if (windowContainer == null) {
             setServerVisible(false);
@@ -234,6 +235,8 @@
         updateSourceFrameForServerVisibility();
 
         if (mOverrideFrameProviders != null) {
+            // Not necessary to clear the mOverrideFrames here. It will be cleared every time the
+            // override frame provider updates.
             for (int i = mOverrideFrameProviders.size() - 1; i >= 0; i--) {
                 final int windowType = mOverrideFrameProviders.keyAt(i);
                 final Rect overrideFrame;
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
index be3ceb8..bf511adf0 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsPersister.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -27,12 +27,12 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.DisplayInfo;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.pm.PackageList;
 import com.android.server.wm.LaunchParamsController.LaunchParams;
diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java
index 16f4377..18a7d2e 100644
--- a/services/core/java/com/android/server/wm/PackageConfigPersister.java
+++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java
@@ -23,12 +23,12 @@
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index f3713eb..ccc71bb 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -19,10 +19,10 @@
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.view.Display;
 import android.view.Display.Mode;
 import android.view.DisplayInfo;
+import android.view.SurfaceControl.RefreshRateRange;
 
 import java.util.HashMap;
 
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index d8b5d78..0ed4835 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -840,11 +840,8 @@
         if (recentsAnimationController != null) {
             recentsAnimationController.checkAnimationReady(defaultDisplay.mWallpaperController);
         }
-        final BackNavigationController backNavigationController =
-                mWmService.mAtmService.mBackNavigationController;
-        if (backNavigationController != null) {
-            backNavigationController.checkAnimationReady(defaultDisplay.mWallpaperController);
-        }
+        mWmService.mAtmService.mBackNavigationController
+                .checkAnimationReady(defaultDisplay.mWallpaperController);
 
         for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5505539..449e77f 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -55,6 +55,7 @@
 import android.window.ScreenCapture;
 
 import com.android.internal.R;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.display.DisplayControl;
 import com.android.server.wm.SurfaceAnimator.AnimationType;
@@ -246,7 +247,7 @@
             HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
                     "ScreenRotationAnimation#getMedianBorderLuma");
-            mStartLuma = RotationAnimationUtils.getMedianBorderLuma(hardwareBuffer,
+            mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer,
                     screenshotBuffer.getColorSpace());
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
 
@@ -489,8 +490,8 @@
             return false;
         }
         if (!mStarted) {
-            mEndLuma = RotationAnimationUtils.getLumaOfSurfaceControl(mDisplayContent.getDisplay(),
-                    mDisplayContent.getWindowingLayer());
+            mEndLuma = TransitionAnimation.getBorderLuma(mDisplayContent.getWindowingLayer(),
+                    finalWidth, finalHeight);
             startAnimation(t, maxAnimationDuration, animationScale, finalWidth, finalHeight,
                     exitAnim, enterAnim);
         }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 885968f..e29e3a2 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -22,7 +22,6 @@
 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_ASSISTANT;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -172,8 +171,6 @@
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
 import android.view.InsetsState;
@@ -198,6 +195,8 @@
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.Watchdog;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.AppTimeTracker;
@@ -5787,12 +5786,10 @@
             return false;
         }
 
-        // Existing Tasks can be reused if a new root task will be created anyway, or for the
-        // Dream - because there can only ever be one DreamActivity.
+        // Existing Tasks can be reused if a new root task will be created anyway.
         final int windowingMode = getWindowingMode();
         final int activityType = getActivityType();
-        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType)
-                || activityType == ACTIVITY_TYPE_DREAM;
+        return DisplayContent.alwaysCreateRootTask(windowingMode, activityType);
     }
 
     void addChild(WindowContainer child, final boolean toTop, boolean showForAllUsers) {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index efb6302..230b760 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2189,7 +2189,7 @@
                 compatScreenHeightDp = inOutConfig.screenHeightDp;
             }
             // Reducing the screen layout starting from its parent config.
-            inOutConfig.screenLayout = computeScreenLayoutOverride(parentConfig.screenLayout,
+            inOutConfig.screenLayout = computeScreenLayout(parentConfig.screenLayout,
                     compatScreenWidthDp, compatScreenHeightDp);
         }
     }
@@ -2252,16 +2252,6 @@
         }
     }
 
-    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
-    static int computeScreenLayoutOverride(int sourceScreenLayout, int screenWidthDp,
-            int screenHeightDp) {
-        sourceScreenLayout = sourceScreenLayout
-                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
-        final int longSize = Math.max(screenWidthDp, screenHeightDp);
-        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
-        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
-    }
-
     @Override
     public int getActivityType() {
         final int applicationType = super.getActivityType();
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 867833a..509b1e6 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -184,19 +184,30 @@
         }
 
         void dispose() {
-            while (!mOrganizedTaskFragments.isEmpty()) {
-                final TaskFragment taskFragment = mOrganizedTaskFragments.get(0);
-                // Cleanup before remove to prevent it from sending any additional event, such as
-                // #onTaskFragmentVanished, to the removed organizer.
+            for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
+                // Cleanup the TaskFragmentOrganizer from all TaskFragments it organized before
+                // removing the windows to prevent it from adding any additional TaskFragment
+                // pending event.
+                final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
                 taskFragment.onTaskFragmentOrganizerRemoved();
-                taskFragment.removeImmediately();
-                mOrganizedTaskFragments.remove(taskFragment);
             }
+
+            // Defer to avoid unnecessary layout when there are multiple TaskFragments removal.
+            mAtmService.deferWindowLayout();
+            try {
+                while (!mOrganizedTaskFragments.isEmpty()) {
+                    final TaskFragment taskFragment = mOrganizedTaskFragments.remove(0);
+                    taskFragment.removeImmediately();
+                }
+            } finally {
+                mAtmService.continueWindowLayout();
+            }
+
             for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) {
                 // Cleanup any running transaction to unblock the current transition.
                 onTransactionFinished(mDeferredTransitions.keyAt(i));
             }
-            mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
+            mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */);
         }
 
         @NonNull
@@ -426,7 +437,6 @@
 
     @Override
     public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
-        validateAndGetState(organizer);
         final int pid = Binder.getCallingPid();
         final long uid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
@@ -607,6 +617,13 @@
             int opType, @NonNull Throwable exception) {
         validateAndGetState(organizer);
         Slog.w(TAG, "onTaskFragmentError ", exception);
+        final PendingTaskFragmentEvent vanishedEvent = taskFragment != null
+                ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED)
+                : null;
+        if (vanishedEvent != null) {
+            // No need to notify if the TaskFragment has been removed.
+            return;
+        }
         addPendingEvent(new PendingTaskFragmentEvent.Builder(
                 PendingTaskFragmentEvent.EVENT_ERROR, organizer)
                 .setErrorCallbackToken(errorCallbackToken)
@@ -690,11 +707,17 @@
     }
 
     private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
-        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get(
+                organizer.asBinder());
+        if (state == null) {
+            Slog.w(TAG, "The organizer has already been removed.");
+            return;
+        }
+        // Remove any pending event of this organizer first because state.dispose() may trigger
+        // event dispatch as result of surface placement.
+        mPendingTaskFragmentEvents.remove(organizer.asBinder());
         // remove all of the children of the organized TaskFragment
         state.dispose();
-        // Remove any pending event of this organizer.
-        mPendingTaskFragmentEvents.remove(organizer.asBinder());
         mTaskFragmentOrganizerState.remove(organizer.asBinder());
     }
 
@@ -878,23 +901,6 @@
         return null;
     }
 
-    private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) {
-        if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR
-                // Always send parent info changed to update task visibility
-                || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
-            return true;
-        }
-
-        final TaskFragmentOrganizerState state =
-                mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder());
-        final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment);
-        final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo();
-        // Send an info changed callback if this event is for the last activities to finish in a
-        // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment.
-        return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED
-                && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty();
-    }
-
     void dispatchPendingEvents() {
         if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()
                 || mPendingTaskFragmentEvents.isEmpty()) {
@@ -908,37 +914,19 @@
         }
     }
 
-    void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
+    private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
             @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
         if (pendingEvents.isEmpty()) {
             return;
         }
-
-        final ArrayList<Task> visibleTasks = new ArrayList<>();
-        final ArrayList<Task> invisibleTasks = new ArrayList<>();
-        final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>();
-        for (int i = 0, n = pendingEvents.size(); i < n; i++) {
-            final PendingTaskFragmentEvent event = pendingEvents.get(i);
-            final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
-            // TODO(b/251132298): move visibility check to the client side.
-            if (task != null && (task.lastActiveTime <= event.mDeferTime
-                    || !(isTaskVisible(task, visibleTasks, invisibleTasks)
-                    || shouldSendEventWhenTaskInvisible(event)))) {
-                // Defer sending events to the TaskFragment until the host task is active again.
-                event.mDeferTime = task.lastActiveTime;
-                continue;
-            }
-            candidateEvents.add(event);
-        }
-        final int numEvents = candidateEvents.size();
-        if (numEvents == 0) {
+        if (shouldDeferPendingEvents(state, pendingEvents)) {
             return;
         }
-
         mTmpTaskSet.clear();
+        final int numEvents = pendingEvents.size();
         final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
         for (int i = 0; i < numEvents; i++) {
-            final PendingTaskFragmentEvent event = candidateEvents.get(i);
+            final PendingTaskFragmentEvent event = pendingEvents.get(i);
             if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED
                     || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
                 final Task task = event.mTaskFragment.getTask();
@@ -954,7 +942,47 @@
         }
         mTmpTaskSet.clear();
         state.dispatchTransaction(transaction);
-        pendingEvents.removeAll(candidateEvents);
+        pendingEvents.clear();
+    }
+
+    /**
+     * Whether or not to defer sending the events to the organizer to avoid waking the app process
+     * when it is in background. We want to either send all events or none to avoid inconsistency.
+     */
+    private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state,
+            @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
+        final ArrayList<Task> visibleTasks = new ArrayList<>();
+        final ArrayList<Task> invisibleTasks = new ArrayList<>();
+        for (int i = 0, n = pendingEvents.size(); i < n; i++) {
+            final PendingTaskFragmentEvent event = pendingEvents.get(i);
+            if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED
+                    && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED
+                    && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) {
+                // Send events for any other types.
+                return false;
+            }
+
+            // Check if we should send the event given the Task visibility and events.
+            final Task task;
+            if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
+                task = event.mTask;
+            } else {
+                task = event.mTaskFragment.getTask();
+            }
+            if (task.lastActiveTime > event.mDeferTime
+                    && isTaskVisible(task, visibleTasks, invisibleTasks)) {
+                // Send events when the app has at least one visible Task.
+                return false;
+            } else if (shouldSendEventWhenTaskInvisible(task, state, event)) {
+                // Sent events even if the Task is invisible.
+                return false;
+            }
+
+            // Defer sending events to the organizer until the host task is active (visible) again.
+            event.mDeferTime = task.lastActiveTime;
+        }
+        // Defer for invisible Task.
+        return true;
     }
 
     private static boolean isTaskVisible(@NonNull Task task,
@@ -975,6 +1003,28 @@
         }
     }
 
+    private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task,
+            @NonNull TaskFragmentOrganizerState state,
+            @NonNull PendingTaskFragmentEvent event) {
+        final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos
+                .get(task.mTaskId);
+        if (lastParentInfo == null || lastParentInfo.isVisible()) {
+            // When the Task was visible, or when there was no Task info changed sent (in which case
+            // the organizer will consider it as visible by default), always send the event to
+            // update the Task visibility.
+            return true;
+        }
+        if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
+            // Send info changed if the TaskFragment is becoming empty/non-empty so the
+            // organizer can choose whether or not to remove the TaskFragment.
+            final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos
+                    .get(event.mTaskFragment);
+            final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0;
+            return lastInfo == null || lastInfo.isEmpty() != isEmpty;
+        }
+        return false;
+    }
+
     void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) {
         final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment,
                 PendingTaskFragmentEvent.EVENT_INFO_CHANGED);
diff --git a/services/core/java/com/android/server/wm/TaskPersister.java b/services/core/java/com/android/server/wm/TaskPersister.java
index 09fd900..29c192c 100644
--- a/services/core/java/com/android/server/wm/TaskPersister.java
+++ b/services/core/java/com/android/server/wm/TaskPersister.java
@@ -30,12 +30,12 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 4459d45..b2c8b7a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -83,11 +83,11 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
+import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.inputmethod.InputMethodManagerInternal;
-import com.android.server.wm.utils.RotationAnimationUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -2190,7 +2190,7 @@
             changeInfo.mSnapshot = snapshotSurface;
             if (isDisplayRotation) {
                 // This isn't cheap, so only do it for display rotations.
-                changeInfo.mSnapshotLuma = RotationAnimationUtils.getMedianBorderLuma(
+                changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma(
                         screenshotBuffer.getHardwareBuffer(), screenshotBuffer.getColorSpace());
             }
             SurfaceControl.Transaction t = wc.mWmService.mTransactionFactory.get();
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 81d6795..6522d93 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -239,8 +239,7 @@
 
     private boolean isBackNavigationTarget(WindowState w) {
         // The window is in animating by back navigation and set to show wallpaper.
-        final BackNavigationController controller = mService.mAtmService.mBackNavigationController;
-        return controller != null && controller.isWallpaperVisible(w);
+        return mService.mAtmService.mBackNavigationController.isWallpaperVisible(w);
     }
 
     /**
@@ -831,9 +830,7 @@
 
             // If there was a pending back navigation animation that would show wallpaper, start
             // the animation due to it was skipped in previous surface placement.
-            if (mService.mAtmService.mBackNavigationController != null) {
-                mService.mAtmService.mBackNavigationController.startAnimation();
-            }
+            mService.mAtmService.mBackNavigationController.startAnimation();
             return true;
         }
         return false;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 9763df6..c4c66d8 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1607,6 +1607,16 @@
         return false;
     }
 
+    /** Computes LONG, SIZE and COMPAT parts of {@link Configuration#screenLayout}. */
+    static int computeScreenLayout(int sourceScreenLayout, int screenWidthDp,
+            int screenHeightDp) {
+        sourceScreenLayout = sourceScreenLayout
+                & (Configuration.SCREENLAYOUT_LONG_MASK | Configuration.SCREENLAYOUT_SIZE_MASK);
+        final int longSize = Math.max(screenWidthDp, screenHeightDp);
+        final int shortSize = Math.min(screenWidthDp, screenHeightDp);
+        return Configuration.reduceScreenLayout(sourceScreenLayout, longSize, shortSize);
+    }
+
     // TODO: Users would have their own window containers under the display container?
     void switchUser(int userId) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 32feb6c..c206a15 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -613,15 +613,6 @@
             @NonNull IBinder imeTargetWindowToken);
 
     /**
-     * Returns the presence of a software navigation bar on the specified display.
-     *
-     * @param displayId the id of display to check if there is a software navigation bar.
-     * @return {@code true} if there is a software navigation. {@code false} otherwise, including
-     *         the case when the specified display does not exist.
-     */
-    public abstract boolean hasNavigationBar(int displayId);
-
-    /**
       * Returns true when the hardware keyboard is available.
       */
     public abstract boolean isHardKeyboardAvailable();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index c17af30..c9d3dac 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -7917,11 +7917,6 @@
         }
 
         @Override
-        public boolean hasNavigationBar(int displayId) {
-            return WindowManagerService.this.hasNavigationBar(displayId);
-        }
-
-        @Override
         public boolean isHardKeyboardAvailable() {
             synchronized (mGlobalLock) {
                 return mHardKeyboardAvailable;
@@ -8703,11 +8698,12 @@
         h.ownerPid = callingPid;
 
         if (region == null) {
-            h.replaceTouchableRegionWithCrop = true;
+            h.replaceTouchableRegionWithCrop(null);
         } else {
             h.touchableRegion.set(region);
+            h.replaceTouchableRegionWithCrop = false;
+            h.setTouchableRegionCrop(surface);
         }
-        h.setTouchableRegionCrop(null /* use the input surface's bounds */);
 
         final SurfaceControl.Transaction t = mTransactionFactory.get();
         t.setInputWindowInfo(surface, h);
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 8055590..7c481f5 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -448,14 +448,8 @@
         if (mFixedRotationTransformState != null) {
             mFixedRotationTransformState.disassociate(this);
         }
-        // TODO(b/233855302): Remove TaskFragment override if the DisplayContent uses the same
-        //  bounds for screenLayout calculation.
-        final Configuration overrideConfig = new Configuration(config);
-        overrideConfig.screenLayout = TaskFragment.computeScreenLayoutOverride(
-                overrideConfig.screenLayout, overrideConfig.screenWidthDp,
-                overrideConfig.screenHeightDp);
         mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
-                overrideConfig, mDisplayContent.getRotation());
+                new Configuration(config), mDisplayContent.getRotation());
         mFixedRotationTransformState.mAssociatedTokens.add(this);
         mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames);
         onFixedRotationStatePrepared();
diff --git a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
index b93b8d8..c11a6d0 100644
--- a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
+++ b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java
@@ -16,24 +16,11 @@
 
 package com.android.server.wm.utils;
 
-import static android.hardware.HardwareBuffer.RGBA_8888;
 import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT;
 
-import android.graphics.Color;
-import android.graphics.ColorSpace;
 import android.graphics.Matrix;
-import android.graphics.Point;
-import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.media.Image;
-import android.media.ImageReader;
-import android.view.Display;
 import android.view.Surface;
-import android.view.SurfaceControl;
-import android.window.ScreenCapture;
-
-import java.nio.ByteBuffer;
-import java.util.Arrays;
 
 
 /** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/
@@ -46,89 +33,6 @@
         return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT;
     }
 
-    /**
-     * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the
-     * luminance at the borders of the bitmap
-     * @return the average luminance of all the pixels at the borders of the bitmap
-     */
-    public static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) {
-        // Cannot read content from buffer with protected usage.
-        if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888
-                || hasProtectedContent(hardwareBuffer)) {
-            return 0;
-        }
-
-        ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(),
-                hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1);
-        ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace);
-        Image image = ir.acquireLatestImage();
-        if (image == null || image.getPlanes().length == 0) {
-            return 0;
-        }
-
-        Image.Plane plane = image.getPlanes()[0];
-        ByteBuffer buffer = plane.getBuffer();
-        int width = image.getWidth();
-        int height = image.getHeight();
-        int pixelStride = plane.getPixelStride();
-        int rowStride = plane.getRowStride();
-        float[] borderLumas = new float[2 * width + 2 * height];
-
-        // Grab the top and bottom borders
-        int l = 0;
-        for (int x = 0; x < width; x++) {
-            borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride);
-        }
-
-        // Grab the left and right borders
-        for (int y = 0; y < height; y++) {
-            borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride);
-            borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride);
-        }
-
-        // Cleanup
-        ir.close();
-
-        // Oh, is this too simple and inefficient for you?
-        // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians
-        Arrays.sort(borderLumas);
-        return borderLumas[borderLumas.length / 2];
-    }
-
-    private static float getPixelLuminance(ByteBuffer buffer, int x, int y,
-            int pixelStride, int rowStride) {
-        int offset = y * rowStride + x * pixelStride;
-        int pixel = 0;
-        pixel |= (buffer.get(offset) & 0xff) << 16;     // R
-        pixel |= (buffer.get(offset + 1) & 0xff) << 8;  // G
-        pixel |= (buffer.get(offset + 2) & 0xff);       // B
-        pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A
-        return Color.valueOf(pixel).luminance();
-    }
-
-    /**
-     * Gets the average border luma by taking a screenshot of the {@param surfaceControl}.
-     * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace)
-     */
-    public static float getLumaOfSurfaceControl(Display display, SurfaceControl surfaceControl) {
-        if (surfaceControl ==  null) {
-            return 0;
-        }
-
-        Point size = new Point();
-        display.getSize(size);
-        Rect crop = new Rect(0, 0, size.x, size.y);
-        ScreenCapture.ScreenshotHardwareBuffer buffer =
-                ScreenCapture.captureLayers(surfaceControl, crop, 1);
-        if (buffer == null) {
-            return 0;
-        }
-
-        return RotationAnimationUtils.getMedianBorderLuma(buffer.getHardwareBuffer(),
-                buffer.getColorSpace());
-    }
-
     public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) {
         switch (rotation) {
             case Surface.ROTATION_0:
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
index f45f626..aa19241 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java
@@ -17,6 +17,11 @@
 package com.android.server.credentials;
 
 import android.annotation.NonNull;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.server.infra.AbstractPerUserSystemService;
@@ -24,7 +29,7 @@
 /**
  * Per-user implementation of {@link CredentialManagerService}
  */
-public class CredentialManagerServiceImpl extends
+public final class CredentialManagerServiceImpl extends
         AbstractPerUserSystemService<CredentialManagerServiceImpl, CredentialManagerService> {
     private static final String TAG = "CredManSysServiceImpl";
 
@@ -34,6 +39,20 @@
         super(master, lock, userId);
     }
 
+    @Override // from PerUserSystemService
+    protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
+            throws PackageManager.NameNotFoundException {
+        ServiceInfo si;
+        try {
+            si = AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
+                    PackageManager.GET_META_DATA, mUserId);
+        } catch (RemoteException e) {
+            throw new PackageManager.NameNotFoundException(
+                    "Could not get service for " + serviceComponent);
+        }
+        return si;
+    }
+
     /**
      * Unimplemented getCredentials
      */
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index 222a96d..8047a53 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -47,11 +47,11 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.UserRestrictionsUtils;
 import com.android.server.utils.Slogf;
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java
index cc32c4d..953a9ee 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DeviceManagementResourcesProvider.java
@@ -27,10 +27,11 @@
 import android.os.Environment;
 import android.util.AtomicFile;
 import android.util.Log;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import libcore.io.IoUtils;
 
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
index 0305c35..8e430b3 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java
@@ -29,12 +29,12 @@
 import android.util.ArraySet;
 import android.util.DebugUtils;
 import android.util.IndentingPrintWriter;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.JournaledFile;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.utils.Slogf;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index a561307..70422bb 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -332,8 +332,6 @@
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.IWindowManager;
 import android.view.accessibility.AccessibilityManager;
@@ -363,6 +361,8 @@
 import com.android.internal.widget.LockSettingsInternal;
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.PasswordValidationError;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.net.module.util.ProxyUtils;
 import com.android.server.AlarmManagerInternal;
 import com.android.server.LocalServices;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
index 2ab5464..3040af2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
@@ -27,11 +27,11 @@
 import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
index 289ed36..035f762 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
@@ -24,12 +24,12 @@
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9e449ae..b74fedf 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1792,7 +1792,8 @@
                 t.traceBegin("StartStatusBarManagerService");
                 try {
                     statusBar = new StatusBarManagerService(context);
-                    ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);
+                    ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar, false,
+                            DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO);
                 } catch (Throwable e) {
                     reportWtf("starting StatusBarManagerService", e);
                 }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
index 5180786..4ceae96 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
@@ -53,7 +53,8 @@
         ParsedActivity::getTaskAffinity,
         ParsedActivity::getTheme,
         ParsedActivity::getUiOptions,
-        ParsedActivity::isSupportsSizeChanges
+        ParsedActivity::isSupportsSizeChanges,
+        ParsedActivity::getTargetDisplayCategory
     )
 
     override fun mainComponentSubclassExtraParams() = listOf(
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
index ad652df..65b99c5 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
@@ -20,9 +20,9 @@
 import android.os.UserHandle
 import android.util.ArrayMap
 import android.util.SparseArray
-import android.util.TypedXmlPullParser
-import android.util.TypedXmlSerializer
 import android.util.Xml
+import com.android.modules.utils.TypedXmlPullParser
+import com.android.modules.utils.TypedXmlSerializer
 import com.android.server.pm.verify.domain.DomainVerificationPersistence
 import com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState
 import com.android.server.pm.verify.domain.models.DomainVerificationPkgState
diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS
index 2bb1649..4dda51f 100644
--- a/services/tests/mockingservicestests/OWNERS
+++ b/services/tests/mockingservicestests/OWNERS
@@ -1,5 +1,8 @@
 include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
+
+# Game Platform
 per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS
 per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS
 per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS
 per-file Game* = file:/GAME_MANAGER_OWNERS
+per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS
diff --git a/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in.xml
new file mode 100644
index 0000000..77fe786
--- /dev/null
+++ b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<game-mode-config
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:supportsPerformanceGameMode="true"
+    android:supportsBatteryGameMode="true"
+    android:allowGameAngleDriver="false"
+    android:allowGameDownscaling="false"
+    android:allowGameFpsOverride="false"
+/>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_no_opt_in.xml
similarity index 100%
rename from services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_disabled.xml
rename to services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_disabled_no_opt_in.xml
diff --git a/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in.xml
new file mode 100644
index 0000000..96d2878
--- /dev/null
+++ b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<game-mode-config
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:supportsPerformanceGameMode="true"
+    android:supportsBatteryGameMode="true"
+    android:allowGameAngleDriver="true"
+    android:allowGameDownscaling="true"
+    android:allowGameFpsOverride="true"
+/>
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml b/services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_no_opt_in.xml
similarity index 100%
rename from services/tests/mockingservicestests/res/xml/gama_manager_service_metadata_config_enabled.xml
rename to services/tests/mockingservicestests/res/xml/game_manager_service_metadata_config_interventions_enabled_no_opt_in.xml
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index abc32c9..ba414cb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -26,6 +26,8 @@
 import static com.android.server.am.BroadcastQueueTest.getUidForPackage;
 import static com.android.server.am.BroadcastQueueTest.makeManifestReceiver;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -278,7 +280,7 @@
         final long notCachedRunnableAt = queue.getRunnableAt();
         queue.setProcessCached(true);
         final long cachedRunnableAt = queue.getRunnableAt();
-        assertTrue(cachedRunnableAt > notCachedRunnableAt);
+        assertThat(cachedRunnableAt).isGreaterThan(notCachedRunnableAt);
         assertEquals(ProcessList.SCHED_GROUP_BACKGROUND, queue.getPreferredSchedulingGroupLocked());
     }
 
@@ -291,20 +293,30 @@
         final BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
                 PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
 
+        // enqueue a bg-priority broadcast then a fg-priority one
+        final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+        final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone);
+        queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, 0);
+
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
         queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, 0);
 
+        // verify that:
+        // (a) the queue is immediately runnable by existence of a fg-priority broadcast
+        // (b) the next one up is the fg-priority broadcast despite its later enqueue time
         queue.setProcessCached(false);
         assertTrue(queue.isRunnable());
-        assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
+        assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueClockTime);
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
+        assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
 
         queue.setProcessCached(true);
         assertTrue(queue.isRunnable());
-        assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
+        assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueClockTime);
         assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
+        assertEquals(queue.peekNextBroadcastRecord(), airplaneRecord);
     }
 
     /**
@@ -346,12 +358,12 @@
 
         mConstants.MAX_PENDING_BROADCASTS = 128;
         queue.invalidateRunnableAt();
-        assertTrue(queue.getRunnableAt() > airplaneRecord.enqueueTime);
+        assertThat(queue.getRunnableAt()).isGreaterThan(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
 
         mConstants.MAX_PENDING_BROADCASTS = 1;
         queue.invalidateRunnableAt();
-        assertTrue(queue.getRunnableAt() == airplaneRecord.enqueueTime);
+        assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_MAX_PENDING, queue.getRunnableAtReason());
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index c125448..d9a26c6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -549,12 +549,6 @@
                 receivers, false, null, null, userId);
     }
 
-    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
-            List<Object> receivers, IIntentReceiver orderedResultTo, Bundle orderedExtras) {
-        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
-                receivers, true, orderedResultTo, orderedExtras, UserHandle.USER_SYSTEM);
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List<Object> receivers) {
         return makeBroadcastRecord(intent, callerApp, options,
@@ -562,12 +556,24 @@
     }
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List<Object> receivers, IIntentReceiver resultTo) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, false, resultTo, null, UserHandle.USER_SYSTEM);
+    }
+
+    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List<Object> receivers, IIntentReceiver resultTo, Bundle resultExtras) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, true, resultTo, resultExtras, UserHandle.USER_SYSTEM);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List<Object> receivers, boolean ordered,
-            IIntentReceiver orderedResultTo, Bundle orderedExtras, int userId) {
+            IIntentReceiver resultTo, Bundle resultExtras, int userId) {
         return new BroadcastRecord(mQueue, intent, callerApp, callerApp.info.packageName, null,
                 callerApp.getPid(), callerApp.info.uid, false, null, null, null, null,
-                AppOpsManager.OP_NONE, options, receivers, callerApp, orderedResultTo,
-                Activity.RESULT_OK, null, orderedExtras, ordered, false, false, userId, false, null,
+                AppOpsManager.OP_NONE, options, receivers, callerApp, resultTo,
+                Activity.RESULT_OK, null, resultExtras, ordered, false, false, userId, false, null,
                 false, null);
     }
 
@@ -1347,6 +1353,26 @@
     }
 
     /**
+     * Verify that we deliver results for unordered broadcasts.
+     */
+    @Test
+    public void testUnordered_ResultTo() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final IApplicationThread callerThread = callerApp.getThread();
+
+        final IIntentReceiver resultTo = mock(IIntentReceiver.class);
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)), resultTo));
+
+        waitForIdle();
+        verify(callerThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)),
+                eq(Activity.RESULT_OK), any(), any(), eq(false),
+                anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt());
+    }
+
+    /**
      * Verify that we're not surprised by a process attempting to finishing a
      * broadcast when none is in progress.
      */
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index 9022db8..d78f6d83 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -73,7 +73,9 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
 
@@ -93,6 +95,7 @@
     private static final String PACKAGE_NAME_INVALID = "com.android.app";
     private static final int USER_ID_1 = 1001;
     private static final int USER_ID_2 = 1002;
+    private static final int DEFAULT_PACKAGE_UID = 12345;
 
     private MockitoSession mMockingSession;
     private String mPackageName;
@@ -207,6 +210,8 @@
                 .thenReturn(packages);
         when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
                 .thenReturn(applicationInfo);
+        when(mMockPackageManager.getPackageUidAsUser(mPackageName, USER_ID_1)).thenReturn(
+                DEFAULT_PACKAGE_UID);
         LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
     }
 
@@ -382,38 +387,41 @@
                 .thenReturn(applicationInfo);
     }
 
-    private void mockInterventionsEnabledFromXml() throws Exception {
-        final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser(
-                mPackageName, PackageManager.GET_META_DATA, USER_ID_1);
-        Bundle metaDataBundle = new Bundle();
-        final int resId = 123;
-        metaDataBundle.putInt(
-                GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId);
-        applicationInfo.metaData = metaDataBundle;
-        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(applicationInfo);
-        seedGameManagerServiceMetaDataFromFile(mPackageName, resId,
-                "res/xml/gama_manager_service_metadata_config_enabled.xml");
+    private void mockInterventionsEnabledNoOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_enabled_no_opt_in.xml");
     }
 
-    private void mockInterventionsDisabledFromXml() throws Exception {
-        final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser(
-                mPackageName, PackageManager.GET_META_DATA, USER_ID_1);
-        Bundle metaDataBundle = new Bundle();
-        final int resId = 123;
-        metaDataBundle.putInt(
-                GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId);
-        applicationInfo.metaData = metaDataBundle;
-        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
-                .thenReturn(applicationInfo);
-        seedGameManagerServiceMetaDataFromFile(mPackageName, resId,
-                "res/xml/gama_manager_service_metadata_config_disabled.xml");
+    private void mockInterventionsEnabledAllOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_enabled_all_opt_in"
+                        + ".xml");
+    }
+
+    private void mockInterventionsDisabledNoOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_disabled_no_opt_in"
+                        + ".xml");
+    }
+
+    private void mockInterventionsDisabledAllOptInFromXml() throws Exception {
+        seedGameManagerServiceMetaDataFromFile(mPackageName, 123,
+                "res/xml/game_manager_service_metadata_config_interventions_disabled_all_opt_in"
+                        + ".xml");
     }
 
 
     private void seedGameManagerServiceMetaDataFromFile(String packageName, int resId,
             String fileName)
             throws Exception {
+        final ApplicationInfo applicationInfo = mMockPackageManager.getApplicationInfoAsUser(
+                mPackageName, PackageManager.GET_META_DATA, USER_ID_1);
+        Bundle metaDataBundle = new Bundle();
+        metaDataBundle.putInt(
+                GameManagerService.GamePackageConfiguration.METADATA_GAME_MODE_CONFIG, resId);
+        applicationInfo.metaData = metaDataBundle;
+        when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(applicationInfo);
         AssetManager assetManager =
                 InstrumentationRegistry.getInstrumentation().getContext().getAssets();
         XmlResourceParser xmlResourceParser =
@@ -641,6 +649,12 @@
         assertEquals(fps, config.getGameModeConfiguration(gameMode).getFps());
     }
 
+    private boolean checkOptedIn(GameManagerService gameManagerService, int gameMode) {
+        GameManagerService.GamePackageConfiguration config =
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
+        return config.willGamePerformOptimizations(gameMode);
+    }
+
     /**
      * Phenotype device config exists, but is only propagating the default value.
      */
@@ -756,7 +770,7 @@
      * Override device configs for both battery and performance modes exists and are valid.
      */
     @Test
-    public void testSetDeviceOverrideConfigAll() {
+    public void testSetDeviceConfigOverrideAll() {
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
 
@@ -776,6 +790,75 @@
         checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 60);
     }
 
+    @Test
+    public void testSetBatteryModeConfigOverride_thenUpdateAllDeviceConfig() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90:mode=3,downscaleFactor=0.1,fps=30";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
+        startUser(gameManagerService, USER_ID_1);
+
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 1.0f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.1f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 30);
+
+        gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, 3, "40",
+                "0.2");
+
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 40);
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.2f);
+
+        String configStringAfter =
+                "mode=2,downscaleFactor=0.9,fps=60:mode=3,downscaleFactor=0.3,fps=50";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringAfter);
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+
+        // performance mode was not overridden thus it should be updated
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0.9f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 60);
+
+        // battery mode was overridden thus it should be the same as the override
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.2f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 40);
+    }
+
+    @Test
+    public void testSetBatteryModeConfigOverride_thenOptInBatteryMode() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90:mode=3,downscaleFactor=0.1,fps=30";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsDisabledNoOptInFromXml();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
+        startUser(gameManagerService, USER_ID_1);
+
+        assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
+        assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_BATTERY));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
+
+        gameManagerService.setGameModeConfigOverride(mPackageName, USER_ID_1, 3, "40",
+                "0.2");
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
+        // override will enable the interventions
+        checkDownscaling(gameManagerService, GameManager.GAME_MODE_BATTERY, 0.2f);
+        checkFps(gameManagerService, GameManager.GAME_MODE_BATTERY, 40);
+
+        mockInterventionsDisabledAllOptInFromXml();
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+
+        assertTrue(checkOptedIn(gameManagerService, GameManager.GAME_MODE_PERFORMANCE));
+        // opt-in is still false for battery mode as override exists
+        assertFalse(checkOptedIn(gameManagerService, GameManager.GAME_MODE_BATTERY));
+    }
+
     /**
      * Override device config for performance mode exists and is valid.
      */
@@ -1050,7 +1133,7 @@
         gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
         assertEquals(GameManager.GAME_MODE_PERFORMANCE,
                 gameManagerService.getGameMode(mPackageName, USER_ID_1));
-        mockInterventionsEnabledFromXml();
+        mockInterventionsEnabledNoOptInFromXml();
         checkLoadingBoost(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
     }
 
@@ -1058,7 +1141,7 @@
     public void testGameModeConfigAllowFpsTrue() throws Exception {
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
-        mockInterventionsEnabledFromXml();
+        mockInterventionsEnabledNoOptInFromXml();
         GameManagerService gameManagerService = new GameManagerService(mMockContext,
                 mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_1);
@@ -1073,7 +1156,7 @@
     public void testGameModeConfigAllowFpsFalse() throws Exception {
         mockDeviceConfigAll();
         mockModifyGameModeGranted();
-        mockInterventionsDisabledFromXml();
+        mockInterventionsDisabledNoOptInFromXml();
         GameManagerService gameManagerService = new GameManagerService(mMockContext,
                 mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_1);
@@ -1551,6 +1634,82 @@
         assertFalse(gameManagerService.mHandler.hasEqualMessages(WRITE_SETTINGS, USER_ID_1));
     }
 
+    @Test
+    public void testResetInterventions_onDeviceConfigReset() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = Mockito.spy(new GameManagerService(mMockContext,
+                mTestLooper.getLooper()));
+        startUser(gameManagerService, USER_ID_1);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(90.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+
+        String configStringAfter = "";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringAfter);
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(0.0f));
+    }
+
+    @Test
+    public void testResetInterventions_onInterventionsDisabled() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = Mockito.spy(new GameManagerService(mMockContext,
+                mTestLooper.getLooper()));
+        startUser(gameManagerService, USER_ID_1);
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(90.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+
+        mockInterventionsDisabledNoOptInFromXml();
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(0.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 0);
+    }
+
+    @Test
+    public void testResetInterventions_onGameModeOptedIn() throws Exception {
+        mockModifyGameModeGranted();
+        String configStringBefore =
+                "mode=2,downscaleFactor=1.0,fps=90";
+        when(DeviceConfig.getProperty(anyString(), anyString()))
+                .thenReturn(configStringBefore);
+        mockInterventionsEnabledNoOptInFromXml();
+        GameManagerService gameManagerService = Mockito.spy(new GameManagerService(mMockContext,
+                mTestLooper.getLooper()));
+        startUser(gameManagerService, USER_ID_1);
+
+        gameManagerService.setGameMode(mPackageName, GameManager.GAME_MODE_PERFORMANCE, USER_ID_1);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(90.0f));
+        checkFps(gameManagerService, GameManager.GAME_MODE_PERFORMANCE, 90);
+
+        mockInterventionsEnabledAllOptInFromXml();
+        gameManagerService.updateConfigsForUser(USER_ID_1, false, mPackageName);
+        Mockito.verify(gameManagerService).setOverrideFrameRate(
+                ArgumentMatchers.eq(DEFAULT_PACKAGE_UID),
+                ArgumentMatchers.eq(0.0f));
+    }
+
     private static void deleteFolder(File folder) {
         File[] files = folder.listFiles();
         if (files != null) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
index 7111047..e08a715 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java
@@ -36,13 +36,14 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 3866da3..d41ac70 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -35,7 +35,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -43,6 +42,8 @@
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.SurfaceControl;
+import android.view.SurfaceControl.RefreshRateRange;
+import android.view.SurfaceControl.RefreshRateRanges;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -75,6 +76,11 @@
     private static final int PORT_A = 0;
     private static final int PORT_B = 0x80;
     private static final int PORT_C = 0xFF;
+    private static final float REFRESH_RATE = 60f;
+    private static final RefreshRateRange REFRESH_RATE_RANGE =
+            new RefreshRateRange(REFRESH_RATE, REFRESH_RATE);
+    private static final RefreshRateRanges REFRESH_RATE_RANGES =
+            new RefreshRateRanges(REFRESH_RATE_RANGE, REFRESH_RATE_RANGE);
 
     private static final long HANDLER_WAIT_MS = 100;
 
@@ -697,16 +703,14 @@
                 new DisplayModeDirector.DesiredDisplayModeSpecs(
                         /*baseModeId*/ baseModeId,
                         /*allowGroupSwitching*/ false,
-                        new RefreshRateRange(60f, 60f),
-                        new RefreshRateRange(60f, 60f)
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
         waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
         verify(mSurfaceControlProxy).setDesiredDisplayModeSpecs(display.token,
                 new SurfaceControl.DesiredDisplayModeSpecs(
                         /* baseModeId */ 0,
                         /* allowGroupSwitching */ false,
-                        /* primaryRange */ 60f, 60f,
-                        /* appRange */ 60f, 60f
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
 
         // Change the display
@@ -732,8 +736,7 @@
                 new DisplayModeDirector.DesiredDisplayModeSpecs(
                         /*baseModeId*/ baseModeId,
                         /*allowGroupSwitching*/ false,
-                        new RefreshRateRange(60f, 60f),
-                        new RefreshRateRange(60f, 60f)
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
 
         waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
@@ -743,8 +746,7 @@
                 new SurfaceControl.DesiredDisplayModeSpecs(
                         /* baseModeId */ 2,
                         /* allowGroupSwitching */ false,
-                        /* primaryRange */ 60f, 60f,
-                        /* appRange */ 60f, 60f
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
                 ));
     }
 
@@ -922,12 +924,11 @@
         }
 
         public SurfaceControl.DesiredDisplayModeSpecs desiredDisplayModeSpecs =
-                new SurfaceControl.DesiredDisplayModeSpecs(/* defaultMode */ 0,
-                    /* allowGroupSwitching */ false,
-                    /* primaryRefreshRateMin */ 60.f,
-                    /* primaryRefreshRateMax */ 60.f,
-                    /* appRefreshRateMin */ 60.f,
-                    /* appRefreshRateMax */60.f);
+                new SurfaceControl.DesiredDisplayModeSpecs(
+                        /* defaultMode */ 0,
+                        /* allowGroupSwitching */ false,
+                        REFRESH_RATE_RANGES, REFRESH_RATE_RANGES
+                );
 
         private FakeDisplay(int port) {
             address = createDisplayAddress(port);
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index 5fb3a4e..7fd1ddb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -69,8 +69,6 @@
 import android.testing.TestableContext;
 import android.util.Log;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 
@@ -80,6 +78,8 @@
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
 import com.android.internal.R;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 import com.android.server.wallpaper.WallpaperManagerService.WallpaperData;
 import com.android.server.wm.WindowManagerInternal;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index a09d994..8a932f1 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -123,6 +123,7 @@
         ":PackageParserTestApp3",
         ":PackageParserTestApp4",
         ":PackageParserTestApp5",
+        ":PackageParserTestApp6",
         ":apex.test",
         ":test.rebootless_apex_v1",
         ":test.rebootless_apex_v2",
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
index b33e22f..9acc4bd 100644
--- a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
@@ -49,13 +49,13 @@
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.AtomicFile;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.widget.RemoteViews;
 
 import com.android.frameworks.servicestests.R;
 import com.android.internal.appwidget.IAppWidgetHost;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.mockito.ArgumentCaptor;
diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
index 581a2a7..2d7d46f 100644
--- a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.fail;
 
 import android.app.backup.BackupTransport;
+import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
 import android.app.backup.RestoreSet;
 import android.content.Intent;
@@ -254,6 +255,9 @@
             ITransportStatusCallback c) throws RemoteException {}
         @Override public void abortFullRestore(ITransportStatusCallback c) throws RemoteException {}
         @Override public void getTransportFlags(AndroidFuture<Integer> f) throws RemoteException {}
+        @Override
+        public void getBackupManagerMonitor(AndroidFuture<IBackupManagerMonitor> resultFuture)
+                throws RemoteException {}
         @Override public IBinder asBinder() {
             return null;
         }
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 eb131419..ffacbf3 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
@@ -16,7 +16,7 @@
 
 package com.android.server.biometrics.sensors;
 
-import static android.testing.TestableLooper.RunWithLooper;
+import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
 
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
@@ -24,8 +24,10 @@
 import static org.junit.Assert.assertEquals;
 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.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -35,6 +37,7 @@
 import static org.mockito.Mockito.withSettings;
 
 import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.IBiometricService;
 import android.os.Binder;
@@ -63,27 +66,25 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.function.Supplier;
 
 @Presubmit
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
-@RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class BiometricSchedulerTest {
 
     private static final String TAG = "BiometricSchedulerTest";
     private static final int TEST_SENSOR_ID = 1;
     private static final int LOG_NUM_RECENT_OPERATIONS = 2;
-
-    private BiometricScheduler mScheduler;
-    private IBinder mToken;
-
-    @Mock
-    private IBiometricService mBiometricService;
-
     @Rule
     public final TestableContext mContext =
             new TestableContext(InstrumentationRegistry.getContext(), null);
+    private BiometricScheduler mScheduler;
+    private IBinder mToken;
+    @Mock
+    private IBiometricService mBiometricService;
 
     @Before
     public void setUp() {
@@ -323,7 +324,7 @@
         client1.getCallback().onClientFinished(client1, true /* success */);
         waitForIdle();
         verify(callback).onError(anyInt(), anyInt(),
-                eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED),
+                eq(BIOMETRIC_ERROR_CANCELED),
                 eq(0) /* vendorCode */);
         assertNull(mScheduler.getCurrentClient());
         assertTrue(client1.isAlreadyDone());
@@ -484,7 +485,7 @@
         mScheduler.scheduleClientMonitor(interrupter);
         waitForIdle();
 
-        verify((Interruptable) interruptableMonitor).cancel();
+        verify(interruptableMonitor).cancel();
         mScheduler.getInternalCallback().onClientFinished(interruptableMonitor, true /* success */);
     }
 
@@ -500,7 +501,7 @@
         mScheduler.scheduleClientMonitor(interrupter);
         waitForIdle();
 
-        verify((Interruptable) interruptableMonitor, never()).cancel();
+        verify(interruptableMonitor, never()).cancel();
     }
 
     @Test
@@ -514,21 +515,180 @@
         assertTrue(client.mDestroyed);
     }
 
+    @Test
+    public void testClearBiometricQueue_clearsHungAuthOperation() {
+        // Creating a hung client
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+        waitForIdle();
+
+        mScheduler.startWatchdog();
+        waitForIdle();
+
+        //Checking client is hung
+        verify(callback1).onClientStarted(client1);
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+        assertNotNull(mScheduler.mCurrentOperation);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        // The hung client did not honor this operation, verify onError and authenticated
+        // were never called.
+        assertFalse(client1.mOnErrorCalled);
+        assertFalse(client1.mAuthenticateCalled);
+        verify(callback1).onClientFinished(client1, false /* success */);
+        assertNull(mScheduler.mCurrentOperation);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+    }
+
+    @Test
+    public void testAuthWorks_afterClearBiometricQueue() {
+        // Creating a hung client
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+
+        assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        //Checking client is hung
+        waitForIdle();
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+
+        // The watchdog should kick off the cancellation
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        // The hung client did not honor this operation, verify onError and authenticated
+        // were never called.
+        assertFalse(client1.mOnErrorCalled);
+        assertFalse(client1.mAuthenticateCalled);
+        verify(callback1).onClientFinished(client1, false /* success */);
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+        assertNull(mScheduler.mCurrentOperation);
+
+
+        //Run additional auth client
+        final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback2 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client2, callback2);
+
+        assertEquals(client2, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(0, mScheduler.getCurrentPendingCount());
+
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        waitForIdle();
+
+        //Ensure auth client passes
+        verify(callback2).onClientStarted(client2);
+        client2.getCallback().onClientFinished(client2, true);
+        waitForIdle();
+
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        //Asserting auth client passes
+        assertTrue(client2.isAlreadyDone());
+        assertNotNull(mScheduler.mCurrentOperation);
+    }
+
+    @Test
+    public void testClearBiometricQueue_doesNotClearOperationsWhenQueueNotStuck() {
+        //Creating clients
+        final TestableLooper looper = TestableLooper.get(this);
+        final Supplier<Object> lazyDaemon1 = () -> mock(Object.class);
+        final TestAuthenticationClient client1 = new TestAuthenticationClient(mContext,
+                lazyDaemon1, mToken, mock(ClientMonitorCallbackConverter.class), 0 /* cookie */);
+        final ClientMonitorCallback callback1 = mock(ClientMonitorCallback.class);
+
+        mScheduler.scheduleClientMonitor(client1, callback1);
+        //Start watchdog
+        mScheduler.startWatchdog();
+        waitForIdle();
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+                mock(ClientMonitorCallback.class));
+        waitForIdle();
+
+        assertEquals(client1, mScheduler.mCurrentOperation.getClientMonitor());
+        assertEquals(2, mScheduler.getCurrentPendingCount());
+        verify(callback1, never()).onClientFinished(any(), anyBoolean());
+        verify(callback1).onClientStarted(client1);
+
+        //Client finishes successfully
+        client1.getCallback().onClientFinished(client1, true);
+        waitForIdle();
+
+        // The watchdog should kick off the cancellation
+        looper.moveTimeForward(10000);
+        waitForIdle();
+        // After 10 seconds the HAL has 3 seconds to respond to a cancel
+        looper.moveTimeForward(3000);
+        waitForIdle();
+
+        //Watchdog does not clear pending operations
+        assertEquals(1, mScheduler.getCurrentPendingCount());
+        assertNotNull(mScheduler.mCurrentOperation);
+
+    }
+
     private BiometricSchedulerProto getDump(boolean clearSchedulerBuffer) throws Exception {
         return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
     }
 
+    private void waitForIdle() {
+        TestableLooper.get(this).processAllMessages();
+    }
+
     private static class TestAuthenticationClient extends AuthenticationClient<Object> {
         boolean mStartedHal = false;
         boolean mStoppedHal = false;
         boolean mDestroyed = false;
         int mNumCancels = 0;
+        boolean mAuthenticateCalled = false;
+        boolean mOnErrorCalled = false;
 
-        public TestAuthenticationClient(@NonNull Context context,
+        TestAuthenticationClient(@NonNull Context context,
                 @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
                 @NonNull ClientMonitorCallbackConverter listener) {
+            this(context, lazyDaemon, token, listener, 1 /* cookie */);
+        }
+
+        TestAuthenticationClient(@NonNull Context context,
+                @NonNull Supplier<Object> lazyDaemon, @NonNull IBinder token,
+                @NonNull ClientMonitorCallbackConverter listener, int cookie) {
             super(context, lazyDaemon, token, listener, 0 /* targetUserId */, 0 /* operationId */,
-                    false /* restricted */, TAG, 1 /* cookie */, false /* requireConfirmation */,
+                    false /* restricted */, TAG, cookie, false /* requireConfirmation */,
                     TEST_SENSOR_ID, mock(BiometricLogger.class), mock(BiometricContext.class),
                     true /* isStrongBiometric */, null /* taskStackListener */,
                     mock(LockoutTracker.class), false /* isKeyguard */,
@@ -546,7 +706,19 @@
         }
 
         @Override
-        protected void handleLifecycleAfterAuth(boolean authenticated) {}
+        protected void handleLifecycleAfterAuth(boolean authenticated) {
+        }
+
+        @Override
+        public void onAuthenticated(BiometricAuthenticator.Identifier identifier,
+                boolean authenticated, ArrayList<Byte> hardwareAuthToken) {
+            mAuthenticateCalled = true;
+        }
+
+        @Override
+        protected void onErrorInternal(int errorCode, int vendorCode, boolean finish) {
+            mOnErrorCalled = true;
+        }
 
         @Override
         public boolean wasUserDetected() {
@@ -651,8 +823,4 @@
             mDestroyed = true;
         }
     }
-
-    private void waitForIdle() {
-        TestableLooper.get(this).processAllMessages();
-    }
 }
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 6b8c26d..d2f2af1 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
@@ -16,6 +16,8 @@
 
 package com.android.server.companion.virtual;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +27,7 @@
 
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.IInputManager;
+import android.hardware.input.InputManager;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -88,6 +91,30 @@
     }
 
     @Test
+    public void registerInputDevice_deviceCreation_hasDeviceId() {
+        final IBinder device1Token = new Binder("device1");
+        mInputController.createMouse("mouse", /*vendorId= */ 1, /*productId= */ 1, device1Token,
+                /* displayId= */ 1);
+        int device1Id = mInputController.getInputDeviceId(device1Token);
+
+        final IBinder device2Token = new Binder("device2");
+        mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2,
+                device2Token, 2);
+        int device2Id = mInputController.getInputDeviceId(device2Token);
+
+        assertWithMessage("Different devices should have different id").that(
+                device1Id).isNotEqualTo(device2Id);
+
+
+        int[] deviceIds = InputManager.getInstance().getInputDeviceIds();
+        assertWithMessage("InputManager's deviceIds list should contain id of device 1").that(
+                deviceIds).asList().contains(device1Id);
+        assertWithMessage("InputManager's deviceIds list should contain id of device 2").that(
+                deviceIds).asList().contains(device2Id);
+
+    }
+
+    @Test
     public void unregisterInputDevice_allMiceUnregistered_clearPointerDisplayId() {
         final IBinder deviceToken = new Binder();
         mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
@@ -115,4 +142,5 @@
         mInputController.unregisterInputDevice(deviceToken);
         verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1));
     }
+
 }
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 9c5d1a5..02bbe65 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
@@ -121,6 +121,7 @@
     private static final int VENDOR_ID = 5;
     private static final String UNIQUE_ID = "uniqueid";
     private static final String PHYS = "phys";
+    private static final int DEVICE_ID = 42;
     private static final int HEIGHT = 1800;
     private static final int WIDTH = 900;
     private static final Binder BINDER = new Binder("binder");
@@ -530,6 +531,16 @@
     }
 
     @Test
+    public void createVirtualKeyboard_inputDeviceId_obtainFromInputController() {
+        final int fd = 1;
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */ 1, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
+        assertWithMessage(
+                "InputController should return device id from InputDeviceDescriptor").that(
+                mInputController.getInputDeviceId(BINDER)).isEqualTo(DEVICE_ID);
+    }
+
+    @Test
     public void onAudioSessionStarting_hasVirtualAudioController() {
         mDeviceImpl.onVirtualDisplayCreatedLocked(
                 mDeviceImpl.createWindowPolicyController(), DISPLAY_ID);
@@ -576,9 +587,9 @@
         final int fd = 1;
         final int keyCode = KeyEvent.KEYCODE_A;
         final int action = VirtualKeyEvent.ACTION_UP;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 1,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */1, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
+
         mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder().setKeyCode(keyCode)
                 .setAction(action).build());
         verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action);
@@ -601,9 +612,8 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
                 .setButtonCode(buttonCode)
@@ -616,9 +626,8 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -642,9 +651,8 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder()
                 .setRelativeX(x).setRelativeY(y).build());
@@ -656,9 +664,8 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -683,9 +690,8 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
                 .setXAxisMovement(x)
@@ -698,9 +704,8 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 2,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */2, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -731,9 +736,8 @@
         final float x = 100.5f;
         final float y = 200.5f;
         final int action = VirtualTouchEvent.ACTION_UP;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
                 .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType).build());
         verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN,
@@ -750,9 +754,8 @@
         final int action = VirtualTouchEvent.ACTION_UP;
         final float pressure = 1.0f;
         final float majorAxisSize = 10.0f;
-        mInputController.mInputDeviceDescriptors.put(BINDER,
-                new InputController.InputDeviceDescriptor(fd, () -> {}, /* type= */ 3,
-                        /* displayId= */ 1, PHYS));
+        mInputController.addDeviceForTesting(BINDER, fd, /* type= */3, /* displayId= */ 1, PHYS,
+                DEVICE_ID);
         mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder().setX(x)
                 .setY(y).setAction(action).setPointerId(pointerId).setToolType(toolType)
                 .setPressure(pressure).setMajorAxisSize(majorAxisSize).build());
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java
index d58d71f..dc46ff8 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/FactoryResetProtectionPolicyTest.java
@@ -23,13 +23,13 @@
 
 import android.app.admin.FactoryResetProtectionPolicy;
 import android.os.Parcel;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
index 72fac55..d540734 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/PolicyVersionUpgraderTest.java
@@ -33,12 +33,12 @@
 import android.os.IpcDataCache;
 import android.os.Parcel;
 import android.os.UserHandle;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.util.JournaledFile;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.SystemService;
 
 import com.google.common.io.Files;
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
index 1308a3e..7588c79d 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/SystemUpdatePolicyTest.java
@@ -29,13 +29,13 @@
 import android.app.admin.FreezePeriod;
 import android.app.admin.SystemUpdatePolicy;
 import android.os.Parcel;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 6860abf..062bde8 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
 
 import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX;
 
@@ -660,6 +661,117 @@
                 firstDisplayId);
     }
 
+    /** Tests that the virtual device is created in a device display group. */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. This should be added to the previously created display
+        // group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertEquals(
+                "Both displays should be added to the same displayGroup.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
+    /**
+     * Tests that the virtual display is not added to the device display group when
+     * VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
+     */
+    @Test
+    public void createVirtualDisplay_addsDisplaysToOwnDisplayGroups() throws Exception {
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+
+        registerDefaultDisplays(displayManager);
+        when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+        when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+                .thenReturn(PackageManager.PERMISSION_DENIED);
+
+        IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+        when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice))
+                .thenReturn(true);
+        when(virtualDevice.getDeviceId()).thenReturn(1);
+
+        // Create a first virtual display. A display group should be created for this display on the
+        // virtual device.
+        final VirtualDisplayConfig.Builder builder1 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId1 =
+                localService.createVirtualDisplay(
+                        builder1.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+
+        // Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
+        // the display should not be added to the previously created display group.
+        final VirtualDisplayConfig.Builder builder2 =
+                new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+                        .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+                        .setUniqueId("uniqueId --- device display group 1");
+
+        int displayId2 =
+                localService.createVirtualDisplay(
+                        builder2.build(),
+                        mMockAppToken /* callback */,
+                        virtualDevice /* virtualDeviceToken */,
+                        mock(DisplayWindowPolicyController.class),
+                        PACKAGE_NAME);
+        int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+
+        assertNotEquals(
+                "Display 1 should be in the device display group and display 2 in its own display"
+                        + " group.",
+                displayGroupId1,
+                displayGroupId2);
+    }
+
     @Test
     public void testGetDisplayIdToMirror() throws Exception {
         DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 18dd264..2b069e3 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -57,7 +57,6 @@
 import android.hardware.display.DisplayManager.DisplayListener;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation;
-import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.hardware.fingerprint.IUdfpsHbmListener;
 import android.os.Handler;
 import android.os.IThermalEventListener;
@@ -71,10 +70,11 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.SurfaceControl.RefreshRateRange;
+import android.view.SurfaceControl.RefreshRateRanges;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.Preconditions;
@@ -105,8 +105,11 @@
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnitParamsRunner.class)
 public class DisplayModeDirectorTest {
     // The tolerance within which we consider something approximately equals.
     private static final String TAG = "DisplayModeDirectorTest";
@@ -154,8 +157,6 @@
 
     private DisplayModeDirector createDirectorFromRefreshRateArray(
             float[] refreshRates, int baseModeId, float defaultRefreshRate) {
-        DisplayModeDirector director =
-                new DisplayModeDirector(mContext, mHandler, mInjector);
         Display.Mode[] modes = new Display.Mode[refreshRates.length];
         Display.Mode defaultMode = null;
         for (int i = 0; i < refreshRates.length; i++) {
@@ -194,13 +195,24 @@
     }
 
     @Test
-    public void testDisplayModeVoting() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testDisplayModeVoting(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         // With no votes present, DisplayModeDirector should allow any refresh rate.
-        DesiredDisplayModeSpecs modeSpecs =
-                createDirectorFromFpsRange(60, 90).getDesiredDisplayModeSpecs(DISPLAY_ID);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        DesiredDisplayModeSpecs modeSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(modeSpecs.baseModeId).isEqualTo(60);
-        assertThat(modeSpecs.primaryRefreshRateRange.min).isEqualTo(0f);
-        assertThat(modeSpecs.primaryRefreshRateRange.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.primary.physical.min).isEqualTo(0f);
+        assertThat(modeSpecs.primary.physical.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.primary.render.min).isEqualTo(0f);
+        assertThat(modeSpecs.primary.render.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.appRequest.physical.min).isEqualTo(0f);
+        assertThat(modeSpecs.appRequest.physical.max).isEqualTo(Float.POSITIVE_INFINITY);
+        assertThat(modeSpecs.appRequest.render.min).isEqualTo(0f);
+        assertThat(modeSpecs.appRequest.render.max).isEqualTo(Float.POSITIVE_INFINITY);
 
         int numPriorities =
                 DisplayModeDirector.Vote.MAX_PRIORITY - DisplayModeDirector.Vote.MIN_PRIORITY + 1;
@@ -210,21 +222,49 @@
         {
             int minFps = 60;
             int maxFps = 90;
-            DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+            director = createDirectorFromFpsRange(60, 90);
             assertTrue(2 * numPriorities < maxFps - minFps + 1);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
             votesByDisplay.put(DISPLAY_ID, votes);
             for (int i = 0; i < numPriorities; i++) {
                 int priority = Vote.MIN_PRIORITY + i;
-                votes.put(priority, Vote.forRefreshRates(minFps + i, maxFps - i));
+                votes.put(priority, Vote.forPhysicalRefreshRates(minFps + i, maxFps - i));
                 director.injectVotesByDisplay(votesByDisplay);
                 modeSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
                 assertThat(modeSpecs.baseModeId).isEqualTo(minFps + i);
-                assertThat(modeSpecs.primaryRefreshRateRange.min)
+                assertThat(modeSpecs.primary.physical.min)
                         .isEqualTo((float) (minFps + i));
-                assertThat(modeSpecs.primaryRefreshRateRange.max)
+                assertThat(modeSpecs.primary.physical.max)
                         .isEqualTo((float) (maxFps - i));
+                if (frameRateIsRefreshRate) {
+                    assertThat(modeSpecs.primary.render.min)
+                            .isEqualTo((float) (minFps + i));
+                } else {
+                    assertThat(modeSpecs.primary.render.min).isZero();
+                }
+                assertThat(modeSpecs.primary.render.max)
+                        .isEqualTo((float) (maxFps - i));
+                if (priority >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF) {
+                    assertThat(modeSpecs.appRequest.physical.min)
+                            .isEqualTo((float) (minFps + i));
+                    assertThat(modeSpecs.appRequest.physical.max)
+                            .isEqualTo((float) (maxFps - i));
+                    if (frameRateIsRefreshRate) {
+                        assertThat(modeSpecs.appRequest.render.min).isEqualTo(
+                                (float) (minFps + i));
+                    } else {
+                        assertThat(modeSpecs.appRequest.render.min).isZero();
+                    }
+                    assertThat(modeSpecs.appRequest.render.max).isEqualTo(
+                            (float) (maxFps - i));
+                } else {
+                    assertThat(modeSpecs.appRequest.physical.min).isZero();
+                    assertThat(modeSpecs.appRequest.physical.max).isPositiveInfinity();
+                    assertThat(modeSpecs.appRequest.render.min).isZero();
+                    assertThat(modeSpecs.appRequest.render.max).isPositiveInfinity();
+                }
+
             }
         }
 
@@ -232,42 +272,53 @@
         // presence of higher priority votes.
         {
             assertTrue(numPriorities >= 2);
-            DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+            director = createDirectorFromFpsRange(60, 90);
             SparseArray<Vote> votes = new SparseArray<>();
             SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
             votesByDisplay.put(DISPLAY_ID, votes);
-            votes.put(Vote.MAX_PRIORITY, Vote.forRefreshRates(65, 85));
-            votes.put(Vote.MIN_PRIORITY, Vote.forRefreshRates(70, 80));
+            votes.put(Vote.MAX_PRIORITY, Vote.forPhysicalRefreshRates(65, 85));
+            votes.put(Vote.MIN_PRIORITY, Vote.forPhysicalRefreshRates(70, 80));
             director.injectVotesByDisplay(votesByDisplay);
             modeSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
             assertThat(modeSpecs.baseModeId).isEqualTo(70);
-            assertThat(modeSpecs.primaryRefreshRateRange.min).isEqualTo(70f);
-            assertThat(modeSpecs.primaryRefreshRateRange.max).isEqualTo(80f);
+            assertThat(modeSpecs.primary.physical.min).isEqualTo(70f);
+            assertThat(modeSpecs.primary.physical.max).isEqualTo(80f);
         }
     }
 
     @Test
-    public void testVotingWithFloatingPointErrors() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithFloatingPointErrors(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         float error = FLOAT_TOLERANCE / 4;
-        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(0, 60));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE,
-                Vote.forRefreshRates(60 + error, 60 + error));
+                Vote.forPhysicalRefreshRates(60 + error, 60 + error));
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
-                Vote.forRefreshRates(60 - error, 60 - error));
+                Vote.forPhysicalRefreshRates(60 - error, 60 - error));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
     }
 
     @Test
-    public void testFlickerHasLowerPriorityThanUserAndRangeIsSingle() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testFlickerHasLowerPriorityThanUserAndRangeIsSingle(
+            boolean frameRateIsRefreshRate) {
         assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE
                 < Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE
@@ -276,6 +327,7 @@
         assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH
                 > Vote.PRIORITY_LOW_POWER_MODE);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[4];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -295,14 +347,14 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -310,14 +362,14 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(90, 90));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -325,14 +377,14 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
 
         votes.clear();
         appRequestedMode = modes[1];
@@ -340,22 +392,28 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(90, 90));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primary.physical.min);
     }
 
     @Test
-    public void testLPMHasHigherPriorityThanUser() {
-        assertTrue(Vote.PRIORITY_LOW_POWER_MODE > Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
-        assertTrue(Vote.PRIORITY_LOW_POWER_MODE > Vote.PRIORITY_APP_REQUEST_SIZE);
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testLPMHasHigherPriorityThanUser(boolean frameRateIsRefreshRate) {
+        assertTrue(Vote.PRIORITY_LOW_POWER_MODE
+                > Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertTrue(Vote.PRIORITY_LOW_POWER_MODE
+                > Vote.PRIORITY_APP_REQUEST_SIZE);
 
-
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[4];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -375,12 +433,18 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -388,12 +452,18 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(90, 90));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(90);
 
         votes.clear();
         appRequestedMode = modes[3];
@@ -401,12 +471,18 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
         appRequestedMode = modes[1];
@@ -414,26 +490,37 @@
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
                 appRequestedMode.getPhysicalHeight()));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(90, 90));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(4);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(90);
     }
 
     @Test
-    public void testAppRequestRefreshRateRange() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestRefreshRateRange(boolean frameRateIsRefreshRate) {
         // Confirm that the app request range doesn't include flicker or min refresh rate settings,
         // but does include everything else.
         assertTrue(
                 Vote.PRIORITY_FLICKER_REFRESH_RATE
                         < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
-        assertTrue(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE
+        assertTrue(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE
                 < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
-        assertTrue(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE
+        assertTrue(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[3];
         modes[0] = new Display.Mode(
                 /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
@@ -446,25 +533,25 @@
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
         Display.Mode appRequestedMode = modes[1];
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
@@ -474,25 +561,28 @@
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(75);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
     }
 
     void verifySpecsWithRefreshRateSettings(DisplayModeDirector director, float minFps,
-            float peakFps, float defaultFps, float primaryMin, float primaryMax,
-            float appRequestMin, float appRequestMax) {
+            float peakFps, float defaultFps, RefreshRateRanges primary,
+            RefreshRateRanges appRequest) {
         DesiredDisplayModeSpecs specs = director.getDesiredDisplayModeSpecsWithInjectedFpsSettings(
                 minFps, peakFps, defaultFps);
-        assertThat(specs.primaryRefreshRateRange.min).isEqualTo(primaryMin);
-        assertThat(specs.primaryRefreshRateRange.max).isEqualTo(primaryMax);
-        assertThat(specs.appRequestRefreshRateRange.min).isEqualTo(appRequestMin);
-        assertThat(specs.appRequestRefreshRateRange.max).isEqualTo(appRequestMax);
+        assertThat(specs.primary).isEqualTo(primary);
+        assertThat(specs.appRequest).isEqualTo(appRequest);
     }
 
     @Test
-    public void testSpecsFromRefreshRateSettings() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testSpecsFromRefreshRateSettings(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         // Confirm that, with varying settings for min, peak, and default refresh rate,
         // DesiredDisplayModeSpecs is calculated correctly.
         float[] refreshRates = {30.f, 60.f, 90.f, 120.f, 150.f};
@@ -500,17 +590,56 @@
                 createDirectorFromRefreshRateArray(refreshRates, /*baseModeId=*/0);
 
         float inf = Float.POSITIVE_INFINITY;
-        verifySpecsWithRefreshRateSettings(director, 0, 0, 0, 0, inf, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 0, 0, 90, 0, 90, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 0, 90, 0, 0, 90, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 0, 90, 60, 0, 60, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 0, 90, 120, 0, 90, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 90, 0, 0, 90, inf, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 90, 0, 120, 90, 120, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 90, 0, 60, 90, inf, 0, inf);
-        verifySpecsWithRefreshRateSettings(director, 90, 120, 0, 90, 120, 0, 120);
-        verifySpecsWithRefreshRateSettings(director, 90, 60, 0, 90, 90, 0, 90);
-        verifySpecsWithRefreshRateSettings(director, 60, 120, 90, 60, 90, 0, 120);
+        RefreshRateRange rangeAll = new RefreshRateRange(0, inf);
+        RefreshRateRange range0to60 = new RefreshRateRange(0, 60);
+        RefreshRateRange range0to90 = new RefreshRateRange(0, 90);
+        RefreshRateRange range0to120 = new RefreshRateRange(0, 120);
+        RefreshRateRange range60to90 = new RefreshRateRange(60, 90);
+        RefreshRateRange range90to90 = new RefreshRateRange(90, 90);
+        RefreshRateRange range90to120 = new RefreshRateRange(90, 120);
+        RefreshRateRange range60toInf = new RefreshRateRange(60, inf);
+        RefreshRateRange range90toInf = new RefreshRateRange(90, inf);
+
+        RefreshRateRanges frameRateAll = new RefreshRateRanges(rangeAll, rangeAll);
+        RefreshRateRanges frameRate90toInf = new RefreshRateRanges(range90toInf, range90toInf);
+        RefreshRateRanges frameRate0to60;
+        RefreshRateRanges frameRate0to90;
+        RefreshRateRanges frameRate0to120;
+        RefreshRateRanges frameRate60to90;
+        RefreshRateRanges frameRate90to90;
+        RefreshRateRanges frameRate90to120;
+        if (frameRateIsRefreshRate) {
+            frameRate0to60 = new RefreshRateRanges(range0to60, range0to60);
+            frameRate0to90 = new RefreshRateRanges(range0to90, range0to90);
+            frameRate0to120 = new RefreshRateRanges(range0to120, range0to120);
+            frameRate60to90 = new RefreshRateRanges(range60to90, range60to90);
+            frameRate90to90 = new RefreshRateRanges(range90to90, range90to90);
+            frameRate90to120 = new RefreshRateRanges(range90to120, range90to120);
+        } else {
+            frameRate0to60 = new RefreshRateRanges(rangeAll, range0to60);
+            frameRate0to90 = new RefreshRateRanges(rangeAll, range0to90);
+            frameRate0to120 = new RefreshRateRanges(rangeAll, range0to120);
+            frameRate60to90 = new RefreshRateRanges(range60toInf, range60to90);
+            frameRate90to90 = new RefreshRateRanges(range90toInf, range90to90);
+            frameRate90to120 = new RefreshRateRanges(range90toInf, range90to120);
+        }
+
+        verifySpecsWithRefreshRateSettings(director, 0, 0, 0, frameRateAll, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 0, 0, 90, frameRate0to90, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 0, frameRate0to90, frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 60, frameRate0to60, frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 0, 90, 120, frameRate0to90,
+                frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 0, frameRate90toInf, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 120, frameRate90to120,
+                frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 90, 0, 60, frameRate90toInf, frameRateAll);
+        verifySpecsWithRefreshRateSettings(director, 90, 120, 0, frameRate90to120,
+                frameRate0to120);
+        verifySpecsWithRefreshRateSettings(director, 90, 60, 0, frameRate90to90,
+                frameRate0to90);
+        verifySpecsWithRefreshRateSettings(director, 60, 120, 90, frameRate60to90,
+                frameRate0to120);
     }
 
     void verifyBrightnessObserverCall(DisplayModeDirector director, float minFps, float peakFps,
@@ -523,7 +652,12 @@
     }
 
     @Test
-    public void testBrightnessObserverCallWithRefreshRateSettings() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testBrightnessObserverCallWithRefreshRateSettings(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         // Confirm that, with varying settings for min, peak, and default refresh rate, we make the
         // correct call to the brightness observer.
         float[] refreshRates = {60.f, 90.f, 120.f};
@@ -538,7 +672,12 @@
     }
 
     @Test
-    public void testVotingWithAlwaysRespectAppRequest() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithAlwaysRespectAppRequest(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[3];
         modes[0] = new Display.Mode(
                 /*modeId=*/50, /*width=*/1000, /*height=*/1000, 50);
@@ -549,61 +688,94 @@
 
         DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
 
-
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(0, 60));
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE, Vote.forRefreshRates(60, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(60, 90));
         Display.Mode appRequestedMode = modes[2];
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
-        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, Vote.forRefreshRates(60, 60));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(60, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
 
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isFalse();
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
 
         director.setShouldAlwaysRespectAppRequestedMode(true);
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isTrue();
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(50);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.physical.min).isAtMost(50);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.render.min).isAtMost(50);
+        assertThat(desiredSpecs.primary.render.max).isAtLeast(90);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
 
         director.setShouldAlwaysRespectAppRequestedMode(false);
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isFalse();
 
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(60);
     }
 
     @Test
-    public void testVotingWithSwitchingTypeNone() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testVotingWithSwitchingTypeNone(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(0, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE, Vote.forRefreshRates(30, 90));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
-
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(30, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
 
         director.injectVotesByDisplay(votesByDisplay);
         assertThat(director.getModeSwitchingType())
                 .isNotEqualTo(DisplayManager.SWITCHING_TYPE_NONE);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(
+                    60);
+        } else {
+            assertThat(desiredSpecs.appRequest.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(30);
 
         director.setModeSwitchingType(DisplayManager.SWITCHING_TYPE_NONE);
@@ -611,10 +783,14 @@
                 .isEqualTo(DisplayManager.SWITCHING_TYPE_NONE);
 
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(30);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(30);
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(30);
         assertThat(desiredSpecs.baseModeId).isEqualTo(30);
     }
 
@@ -641,7 +817,12 @@
     }
 
     @Test
-    public void testDefaultDisplayModeIsSelectedIfAvailable() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testDefaultDisplayModeIsSelectedIfAvailable(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         final float[] refreshRates = new float[]{24f, 25f, 30f, 60f, 90f};
         final int defaultModeId = 3;
         DisplayModeDirector director = createDirectorFromRefreshRateArray(
@@ -778,7 +959,7 @@
         sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 20 /*lux*/));
 
         Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
-        assertVoteForRefreshRate(vote, 90 /*fps*/);
+        assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
         assertThat(vote.disableRefreshRateSwitching).isTrue();
@@ -847,7 +1028,7 @@
         sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 9000));
 
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
-        assertVoteForRefreshRate(vote, 60 /*fps*/);
+        assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
         assertThat(vote.disableRefreshRateSwitching).isTrue();
@@ -923,12 +1104,17 @@
     }
 
     @Test
-    public void testAppRequestMinRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestMinRefreshRate(boolean frameRateIsRefreshRate) {
         // Confirm that the app min request range doesn't include flicker or min refresh rate
         // settings but does include everything else.
-        assertTrue(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE
+        assertTrue(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[3];
         modes[0] = new Display.Mode(
                 /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
@@ -942,38 +1128,42 @@
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE,
-                Vote.forRefreshRates(75, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(75, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
     }
 
     @Test
-    public void testAppRequestMaxRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestMaxRefreshRate(boolean frameRateIsRefreshRate) {
         // Confirm that the app max request range doesn't include flicker or min refresh rate
         // settings but does include everything else.
-        assertTrue(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE
+        assertTrue(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
         Display.Mode[] modes = new Display.Mode[3];
@@ -984,63 +1174,104 @@
         modes[2] = new Display.Mode(
                 /*modeId=*/90, /*width=*/1000, /*height=*/1000, 90);
 
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromModeArray(modes, modes[1]);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forPhysicalRefreshRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.render.min).isZero();
+        }
+        assertThat(desiredSpecs.primary.render.max).isAtMost(60);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isAtMost(60f);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+        }
+        assertThat(desiredSpecs.appRequest.render.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.render.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.physical.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequest.render.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequest.render.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 75));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(0, 75));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isZero();
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(75);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(75);
+        } else {
+            assertThat(desiredSpecs.primary.render.min).isZero();
+        }
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequest.physical.min).isZero();
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(
+                    75);
+        } else {
+            assertThat(desiredSpecs.appRequest.physical.max).isAtLeast(90f);
+        }
+        assertThat(desiredSpecs.appRequest.render.min).isZero();
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(75);
     }
 
     @Test
-    public void testAppRequestObserver_modeId() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_modeId(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0, 0);
 
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
+                .isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
 
         Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRange.min).isZero();
-        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestSize.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestSize.baseModeRefreshRate).isZero();
+        assertThat(appRequestSize.appRequestBaseModeRefreshRate).isZero();
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNull(appRequestRefreshRateRange);
 
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0, 0);
@@ -1048,27 +1279,37 @@
         appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
+                .isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
 
         appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRange.min).isZero();
-        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
         appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNull(appRequestRefreshRateRange);
     }
 
     @Test
-    public void testAppRequestObserver_minRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_minRefreshRate(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 60, 0);
         Vote appRequestRefreshRate =
@@ -1079,11 +1320,20 @@
         assertNull(appRequestSize);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min)
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min)
+                    .isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max).isAtLeast(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
                 .isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max).isAtLeast(90);
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
 
@@ -1096,17 +1346,32 @@
         assertNull(appRequestSize);
 
         appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min)
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isWithin(
+                    FLOAT_TOLERANCE).of(90);
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max).isAtLeast(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
                 .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max).isAtLeast(90);
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
     }
 
     @Test
-    public void testAppRequestObserver_maxRefreshRate() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_maxRefreshRate(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 90);
         Vote appRequestRefreshRate =
@@ -1117,10 +1382,19 @@
         assertNull(appRequestSize);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
                 .isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
@@ -1134,10 +1408,19 @@
         assertNull(appRequestSize);
 
         appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
                 .isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
@@ -1155,46 +1438,71 @@
         assertNull(appRequestSize);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNull(appRequestRefreshRateRange);
     }
 
     @Test
-    public void testAppRequestObserver_modeIdAndRefreshRateRange() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestObserver_modeIdAndRefreshRateRange(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90, 90);
 
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
+                .isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
 
         Vote appRequestSize =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRange.min).isZero();
-        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
+        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
+        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
         Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
         assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        if (frameRateIsRefreshRate) {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min)
+                    .isWithin(FLOAT_TOLERANCE).of(90);
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
+            assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
+                    .isPositiveInfinity();
+        }
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
                 .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
                 .isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
         assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
     }
 
     @Test
-    public void testAppRequestsIsTheDefaultMode() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testAppRequestsIsTheDefaultMode(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[2];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -1204,8 +1512,8 @@
         DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(1);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.physical.min).isAtMost(60);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90);
 
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
@@ -1214,105 +1522,148 @@
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
-                        appRequestedMode.getPhysicalHeight()));
+                appRequestedMode.getPhysicalHeight()));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.baseModeId).isEqualTo(2);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(60);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+        assertThat(desiredSpecs.primary.physical.min).isAtMost(60);
+        assertThat(desiredSpecs.primary.physical.max).isAtLeast(90);
     }
 
     @Test
-    public void testDisableRefreshRateSwitchingVote() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testDisableRefreshRateSwitchingVote(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(90, Float.POSITIVE_INFINITY));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(50);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(50);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(50);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(50);
         assertThat(desiredSpecs.baseModeId).isEqualTo(50);
 
         votes.clear();
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE,
-                Vote.forRefreshRates(70, Float.POSITIVE_INFINITY));
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(80, Float.POSITIVE_INFINITY));
+                Vote.forPhysicalRefreshRates(70, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(80, Float.POSITIVE_INFINITY));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 90));
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(80);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(80);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(80);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(80);
         assertThat(desiredSpecs.baseModeId).isEqualTo(80);
 
         votes.clear();
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE,
-                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
-        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
-                Vote.forRefreshRates(80, Float.POSITIVE_INFINITY));
+                Vote.forPhysicalRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(80, Float.POSITIVE_INFINITY));
         votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 90));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 90));
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
     }
 
     @Test
-    public void testBaseModeIdInPrimaryRange() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testBaseModeIdInPrimaryRange(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(70));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(desiredSpecs.baseModeId).isEqualTo(50);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(desiredSpecs.baseModeId).isEqualTo(50);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+            assertThat(desiredSpecs.baseModeId).isEqualTo(70);
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(55));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.baseModeId).isEqualTo(55);
 
         votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 52));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(0, 52));
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(55));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+            assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(52);
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
         assertThat(desiredSpecs.baseModeId).isEqualTo(55);
 
         votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 58));
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(0, 58));
         votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forBaseModeRefreshRate(55));
-        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(0, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(58);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(0);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(58);
+        } else {
+            assertThat(desiredSpecs.primary.physical.max).isPositiveInfinity();
+        }
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(58);
         assertThat(desiredSpecs.baseModeId).isEqualTo(55);
     }
 
     @Test
-    public void testStaleAppVote() {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testStaleAppVote(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
         Display.Mode[] modes = new Display.Mode[4];
         modes[0] = new Display.Mode(
                 /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
@@ -1358,9 +1709,124 @@
     }
 
     @Test
-    public void testProximitySensorVoting() throws Exception {
+    @Parameters({
+            "true",
+            "false"
+    })
+    public void testRefreshRateIsSubsetOfFrameRate(boolean frameRateIsRefreshRate) {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(frameRateIsRefreshRate);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 120);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+
+
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+        }
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(
+                    120);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        }
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(60, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(
+                    120);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(60);
+            assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(60);
+        }
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(90, 120));
+        votes.put(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                Vote.forRenderFrameRates(140, 140));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        if (frameRateIsRefreshRate) {
+            assertThat(desiredSpecs.appRequest.render.min).isWithin(FLOAT_TOLERANCE).of(90);
+        } else {
+            assertThat(desiredSpecs.appRequest.render.min).isZero();
+        }
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+    }
+
+    @Test
+    public void testRenderFrameRateIsAchievableByPhysicalRefreshRate() {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(false);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 120);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+
+
+        votes.put(Vote.PRIORITY_UDFPS, Vote.forPhysicalRefreshRates(120, 120));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRenderFrameRates(90, 90));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.appRequest.physical.min).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.appRequest.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.appRequest.render.min).isZero();
+        assertThat(desiredSpecs.appRequest.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+    }
+
+    @Test
+    public void testRenderFrameRateIsDroppedIfLowerPriorityThenBaseModeRefreshRate() {
+        when(mInjector.renderFrameRateIsPhysicalRefreshRate()).thenReturn(false);
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 120);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        votes.put(Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                Vote.forRenderFrameRates(120, 120));
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(90));
+        votes.put(Vote.PRIORITY_PROXIMITY, Vote.forPhysicalRefreshRates(60, 120));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primary.physical.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primary.physical.max).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.primary.render.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primary.render.max).isWithin(FLOAT_TOLERANCE).of(120);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(90);
+    }
+
+    @Test
+    public void testProximitySensorVoting() {
         DisplayModeDirector director =
-                createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0);
+                createDirectorFromRefreshRateArray(new float[]{60.f, 90.f}, 0);
         director.start(createMockSensorManager());
 
         ArgumentCaptor<ProximityActiveListener> ProximityCaptor =
@@ -1389,7 +1855,7 @@
         // Set the proximity to active and verify that we added a vote.
         proximityListener.onProximityActive(true);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_PROXIMITY);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Set the display state to doze and verify that the vote is gone
         when(mInjector.isDozeState(any(Display.class))).thenReturn(true);
@@ -1401,7 +1867,7 @@
         when(mInjector.isDozeState(any(Display.class))).thenReturn(false);
         displayListener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_PROXIMITY);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Set the display state to doze and verify that the vote is gone
         when(mInjector.isDozeState(any(Display.class))).thenReturn(true);
@@ -1412,7 +1878,7 @@
         // Remove the display to cause the doze state to be removed
         displayListener.onDisplayRemoved(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_PROXIMITY);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Turn prox off and verify vote is gone.
         proximityListener.onProximityActive(false);
@@ -1456,7 +1922,7 @@
                     BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, hbmRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, hbmRefreshRate);
 
         // Turn on HBM, with brightness below the HBM range
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1483,7 +1949,7 @@
                     BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, hbmRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, hbmRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1579,7 +2045,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, initialRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, initialRefreshRate);
 
         // Change refresh rate vote value through DeviceConfig, ensure it takes precedence
         final int updatedRefreshRate = 90;
@@ -1589,7 +2055,7 @@
         assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight())
                 .isEqualTo(updatedRefreshRate);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, updatedRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, updatedRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1605,7 +2071,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, updatedRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, updatedRefreshRate);
 
         // Reset DeviceConfig refresh rate, ensure vote falls back to the initial value
         mInjector.getDeviceConfig().setRefreshRateInHbmSunlight(0);
@@ -1613,7 +2079,7 @@
         waitForIdleSync();
         assertThat(director.getHbmObserver().getRefreshRateInHbmSunlight()).isEqualTo(0);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, initialRefreshRate);
+        assertVoteForPhysicalRefreshRate(vote, initialRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1693,7 +2159,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, 60.0f);
+        assertVoteForPhysicalRefreshRate(vote, 60.0f);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
@@ -1741,7 +2207,7 @@
         if (Float.isNaN(rr)) {
             assertNull(vote);
         } else {
-            assertVoteForRefreshRate(vote, rr);
+            assertVoteForPhysicalRefreshRate(vote, rr);
         }
     }
 
@@ -1817,7 +2283,7 @@
                     TRANSITION_POINT, BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
-        assertVoteForRefreshRate(vote, 60.f);
+        assertVoteForPhysicalRefreshRate(vote, 60.f);
 
         // Turn off HBM
         listener.onDisplayRemoved(DISPLAY_ID);
@@ -1845,7 +2311,7 @@
         // Set the skin temperature to critical and verify that we added a vote.
         listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_CRITICAL));
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_SKIN_TEMPERATURE);
-        assertVoteForRefreshRateRange(vote, 0f, 60.f);
+        assertVoteForRenderFrameRateRange(vote, 0f, 60.f);
 
         // Set the skin temperature to severe and verify that the vote is gone.
         listener.notifyThrottling(getSkinTemp(Temperature.THROTTLING_SEVERE));
@@ -1871,18 +2337,18 @@
         return new Temperature(30.0f, Temperature.TYPE_SKIN, "test_skin_temp", status);
     }
 
-    private void assertVoteForRefreshRate(Vote vote, float refreshRate) {
+    private void assertVoteForPhysicalRefreshRate(Vote vote, float refreshRate) {
         assertThat(vote).isNotNull();
         final RefreshRateRange expectedRange = new RefreshRateRange(refreshRate, refreshRate);
-        assertThat(vote.refreshRateRange).isEqualTo(expectedRange);
+        assertThat(vote.refreshRateRanges.physical).isEqualTo(expectedRange);
     }
 
-    private void assertVoteForRefreshRateRange(
-            Vote vote, float refreshRateLow, float refreshRateHigh) {
+    private void assertVoteForRenderFrameRateRange(
+            Vote vote, float frameRateLow, float frameRateHigh) {
         assertThat(vote).isNotNull();
         final RefreshRateRange expectedRange =
-                new RefreshRateRange(refreshRateLow, refreshRateHigh);
-        assertThat(vote.refreshRateRange).isEqualTo(expectedRange);
+                new RefreshRateRange(frameRateLow, frameRateHigh);
+        assertThat(vote.refreshRateRanges.render).isEqualTo(expectedRange);
     }
 
     public static class FakeDeviceConfig extends FakeDeviceConfigInterface {
@@ -2083,6 +2549,11 @@
             return null;
         }
 
+        @Override
+        public boolean renderFrameRateIsPhysicalRefreshRate() {
+            return true;
+        }
+
         void notifyPeakRefreshRateChanged() {
             if (mPeakRefreshRateObserver != null) {
                 mPeakRefreshRateObserver.dispatchChange(false /*selfChange*/,
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 0b33c30..657bda6 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -369,6 +369,98 @@
     }
 
     @Test
+    public void testDevicesAreAddedToDeviceDisplayGroups() {
+        // Create the default internal display of the device.
+        LogicalDisplay defaultDisplay =
+                add(
+                        createDisplayDevice(
+                                Display.TYPE_INTERNAL,
+                                600,
+                                800,
+                                DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY));
+
+        // Create 3 virtual displays associated with a first virtual device.
+        int deviceId1 = 1;
+        TestDisplayDevice display1 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display1", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display1, deviceId1);
+        LogicalDisplay virtualDevice1Display1 = add(display1);
+
+        TestDisplayDevice display2 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display2", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display2, deviceId1);
+        LogicalDisplay virtualDevice1Display2 = add(display2);
+
+        TestDisplayDevice display3 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice1Display3", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display3, deviceId1);
+        LogicalDisplay virtualDevice1Display3 = add(display3);
+
+        // Create another 3 virtual displays associated with a second virtual device.
+        int deviceId2 = 2;
+        TestDisplayDevice display4 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display1", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display4, deviceId2);
+        LogicalDisplay virtualDevice2Display1 = add(display4);
+
+        TestDisplayDevice display5 =
+                createDisplayDevice(Display.TYPE_VIRTUAL, "virtualDevice2Display2", 600, 800, 0);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display5, deviceId2);
+        LogicalDisplay virtualDevice2Display2 = add(display5);
+
+        // The final display is created with FLAG_OWN_DISPLAY_GROUP set.
+        TestDisplayDevice display6 =
+                createDisplayDevice(
+                        Display.TYPE_VIRTUAL,
+                        "virtualDevice2Display3",
+                        600,
+                        800,
+                        DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+        mLogicalDisplayMapper.associateDisplayDeviceWithVirtualDevice(display6, deviceId2);
+        LogicalDisplay virtualDevice2Display3 = add(display6);
+
+        // Verify that the internal display is in the default display group.
+        assertEquals(
+                DEFAULT_DISPLAY_GROUP,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(defaultDisplay)));
+
+        // Verify that all the displays for virtual device 1 are in the same (non-default) display
+        // group.
+        int virtualDevice1DisplayGroupId =
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display1));
+        assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice1DisplayGroupId);
+        assertEquals(
+                virtualDevice1DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display2)));
+        assertEquals(
+                virtualDevice1DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice1Display3)));
+
+        // The first 2 displays for virtual device 2 should be in the same non-default group.
+        int virtualDevice2DisplayGroupId =
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display1));
+        assertNotEquals(DEFAULT_DISPLAY_GROUP, virtualDevice2DisplayGroupId);
+        assertEquals(
+                virtualDevice2DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display2)));
+        // virtualDevice2Display3 was created with FLAG_OWN_DISPLAY_GROUP and shouldn't be grouped
+        // with other displays of this device or be in the default display group.
+        assertNotEquals(
+                virtualDevice2DisplayGroupId,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display3)));
+        assertNotEquals(
+                DEFAULT_DISPLAY_GROUP,
+                mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(
+                        id(virtualDevice2Display3)));
+    }
+
+    @Test
     public void testDeviceShouldBeWoken() {
         assertTrue(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN,
                 DEVICE_STATE_CLOSED,
@@ -416,14 +508,22 @@
     /////////////////
 
     private TestDisplayDevice createDisplayDevice(int type, int width, int height, int flags) {
-        return createDisplayDevice(new TestUtils.TestDisplayAddress(), type, width, height, flags);
+        return createDisplayDevice(
+                new TestUtils.TestDisplayAddress(), /*  uniqueId */ "", type, width, height, flags);
     }
 
     private TestDisplayDevice createDisplayDevice(
-            DisplayAddress address, int type, int width, int height, int flags) {
+            int type, String uniqueId, int width, int height, int flags) {
+        return createDisplayDevice(
+                new TestUtils.TestDisplayAddress(), uniqueId, type, width, height, flags);
+    }
+
+    private TestDisplayDevice createDisplayDevice(
+            DisplayAddress address, String uniqueId, int type, int width, int height, int flags) {
         TestDisplayDevice device = new TestDisplayDevice();
         DisplayDeviceInfo displayDeviceInfo = device.getSourceInfo();
         displayDeviceInfo.type = type;
+        displayDeviceInfo.uniqueId = uniqueId;
         displayDeviceInfo.width = width;
         displayDeviceInfo.height = height;
         displayDeviceInfo.flags = flags;
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
new file mode 100644
index 0000000..303a370
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.dreams;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.service.dreams.IDreamService;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DreamControllerTest {
+    @Mock
+    private DreamController.Listener mListener;
+    @Mock
+    private Context mContext;
+    @Mock
+    private IBinder mIBinder;
+    @Mock
+    private IDreamService mIDreamService;
+
+    @Captor
+    private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor;
+    @Captor
+    private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;
+
+    private final TestLooper mLooper = new TestLooper();
+    private final Handler mHandler = new Handler(mLooper.getLooper());
+
+    private DreamController mDreamController;
+
+    private Binder mToken;
+    private ComponentName mDreamName;
+    private ComponentName mOverlayName;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mIDreamService.asBinder()).thenReturn(mIBinder);
+        when(mIBinder.queryLocalInterface(anyString())).thenReturn(mIDreamService);
+        when(mContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true);
+
+        mToken = new Binder();
+        mDreamName = ComponentName.unflattenFromString("dream");
+        mOverlayName = ComponentName.unflattenFromString("dream_overlay");
+        mDreamController = new DreamController(mContext, mHandler, mListener);
+    }
+
+    @Test
+    public void startDream_attachOnServiceConnected() throws RemoteException {
+        // Call dream controller to start dreaming.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+
+        // Mock service connected.
+        final ServiceConnection serviceConnection = captureServiceConnection();
+        serviceConnection.onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Verify that dream service is called to attach.
+        verify(mIDreamService).attach(eq(mToken), eq(false) /*doze*/, any());
+    }
+
+    @Test
+    public void startDream_startASecondDream_detachOldDreamOnceNewDreamIsStarted()
+            throws RemoteException {
+        // Start first dream.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+        clearInvocations(mContext);
+
+        // Set up second dream.
+        final Binder newToken = new Binder();
+        final ComponentName newDreamName = ComponentName.unflattenFromString("new_dream");
+        final ComponentName newOverlayName = ComponentName.unflattenFromString("new_dream_overlay");
+        final IDreamService newDreamService = mock(IDreamService.class);
+        final IBinder newBinder = mock(IBinder.class);
+        when(newDreamService.asBinder()).thenReturn(newBinder);
+        when(newBinder.queryLocalInterface(anyString())).thenReturn(newDreamService);
+
+        // Start second dream.
+        mDreamController.startDream(newToken, newDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, newOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(newDreamName, newBinder);
+        mLooper.dispatchAll();
+
+        // Mock second dream started.
+        verify(newDreamService).attach(eq(newToken), eq(false) /*doze*/,
+                mRemoteCallbackCaptor.capture());
+        mRemoteCallbackCaptor.getValue().sendResult(null /*data*/);
+        mLooper.dispatchAll();
+
+        // Verify that the first dream is called to detach.
+        verify(mIDreamService).detach();
+    }
+
+    @Test
+    public void stopDream_detachFromService() throws RemoteException {
+        // Start dream.
+        mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+                0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+        captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+        mLooper.dispatchAll();
+
+        // Stop dream.
+        mDreamController.stopDream(true /*immediate*/, "test stop dream" /*reason*/);
+
+        // Verify that dream service is called to detach.
+        verify(mIDreamService).detach();
+    }
+
+    private ServiceConnection captureServiceConnection() {
+        verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
+                any());
+        return mServiceConnectionACaptor.getValue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 545f318..3a57db9 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -19,7 +19,6 @@
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_TV;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -47,7 +46,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 /** Tests for {@link DevicePowerStatusAction} */
@@ -65,7 +63,6 @@
     private FakePowerManagerWrapper mPowerManager;
 
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPhysicalAddress;
 
     private DevicePowerStatusAction mDevicePowerStatusAction;
@@ -79,7 +76,8 @@
 
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
@@ -117,11 +115,8 @@
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
         mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
-        mPlaybackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        mPlaybackDevice.init();
-        mLocalDevices.add(mPlaybackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+        mPlaybackDevice = mHdmiControlService.playback();
         mDevicePowerStatusAction = DevicePowerStatusAction.create(mPlaybackDevice, ADDR_TV,
                 mCallbackMock);
         mTestLooper.dispatchAll();
@@ -213,7 +208,6 @@
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
         mTestLooper.dispatchAll();
 
@@ -238,7 +232,6 @@
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder
                 .buildReportPhysicalAddressCommand(ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV);
         mNativeWrapper.onCecMessage(reportPhysicalAddress);
@@ -263,7 +256,6 @@
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         HdmiCecMessage reportPhysicalAddress = HdmiCecMessageBuilder
                 .buildReportPhysicalAddressCommand(ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV);
         mNativeWrapper.onCecMessage(reportPhysicalAddress);
@@ -293,6 +285,12 @@
 
     @Test
     public void pendingActionDoesNotBlockSendingStandby() throws Exception {
+        HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(
+                mPlaybackDevice.getDeviceInfo().getLogicalAddress(),
+                mPhysicalAddress);
+        assertThat(mPlaybackDevice.handleActiveSource(message))
+                .isEqualTo(Constants.HANDLED);
+
         mPlaybackDevice.addAndStartAction(mDevicePowerStatusAction);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index eb7a761..7df0078 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -27,7 +27,6 @@
 import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_ACTIVE_SOURCE_MESSAGE_AFTER_ROUTING_CHANGE;
 import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_DEVICE_POWER_ON;
 import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_FOR_REPORT_POWER_STATUS;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -86,7 +85,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
     private int mPlaybackLogicalAddress1;
     private int mPlaybackLogicalAddress2;
@@ -101,7 +99,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -119,8 +118,6 @@
                 };
 
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService);
-        mHdmiCecLocalDevicePlayback.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -135,16 +132,14 @@
                 mHdmiCecController, mHdmiMhlControllerStub);
         mHdmiControlService.setHdmiCecNetwork(mHdmiCecNetwork);
 
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
-
+        mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
         // The addresses depend on local device's LA.
         // This help the tests to pass with every local device LA.
         mPlaybackLogicalAddress1 =
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 72d36b0..ac57834 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -26,7 +26,6 @@
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_DEVICE_POWER_ON;
 import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_REPORT_POWER_STATUS;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -101,7 +100,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
     @Before
     public void setUp() {
@@ -110,7 +108,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -127,8 +126,7 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
+
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -136,7 +134,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_PLAYBACK_1,
@@ -149,12 +146,12 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_1);
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_2);
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
     }
 
     private static class TestActionTimer implements ActionTimer {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index 9f744f9..d2fe6da 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -19,7 +19,6 @@
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.Constants.PATH_RELATIONSHIP_ANCESTOR;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -35,6 +34,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.os.Binder;
@@ -55,7 +55,6 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 /**
@@ -68,7 +67,6 @@
     private HdmiCecAtomWriter mHdmiCecAtomWriterSpy;
     private HdmiControlService mHdmiControlServiceSpy;
     private HdmiCecController mHdmiCecController;
-    private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback;
     private HdmiMhlControllerStub mHdmiMhlControllerStub;
     private FakeNativeWrapper mNativeWrapper;
     private FakePowerManagerWrapper mPowerManager;
@@ -77,7 +75,6 @@
     private Context mContextSpy;
     private TestLooper mTestLooper = new TestLooper();
     private int mPhysicalAddress = 0x1110;
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiPortInfo[] mHdmiPortInfo;
 
     @Before
@@ -89,7 +86,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
@@ -123,14 +121,9 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy);
-        mHdmiCecLocalDevicePlayback.init();
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
-
         mHdmiControlServiceSpy.initService();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
 
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 91d265c..08d0e90 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -48,7 +48,6 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
-import java.util.Collections;
 
 @SmallTest
 @Presubmit
@@ -80,15 +79,19 @@
     private HdmiDeviceInfo mDeviceInfo;
     private boolean mArcSupport;
     private HdmiPortInfo[] mHdmiPortInfo;
+    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
 
     @Before
     public void setUp() {
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
 
         mHdmiControlService =
             new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                    Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                    mLocalDeviceTypes,
+                    new FakeAudioDeviceVolumeManagerWrapper()) {
                 @Override
                 AudioManager getAudioManager() {
                     return new AudioManager() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index fe9e0b6..75c4d92 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -47,6 +47,7 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
@@ -78,7 +79,6 @@
     private TestLooper mTestLooper = new TestLooper();
     private FakePowerManagerWrapper mPowerManager;
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
-    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
     private int mPlaybackPhysicalAddress;
     private int mPlaybackLogicalAddress;
     private boolean mWokenUp;
@@ -91,10 +91,10 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
-        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        mLocalDeviceTypes, new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
 
                     @Override
                     void wakeUp() {
@@ -128,8 +128,6 @@
                     }
                 };
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(mHdmiControlService);
-        mHdmiCecLocalDevicePlayback.init();
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setIoLooper(mMyLooper);
         mNativeWrapper = new FakeNativeWrapper();
@@ -137,7 +135,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false);
@@ -148,10 +145,11 @@
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mPlaybackPhysicalAddress = 0x2000;
         mNativeWrapper.setPhysicalAddress(mPlaybackPhysicalAddress);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
+        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mPlaybackLogicalAddress = mHdmiCecLocalDevicePlayback.getDeviceInfo().getLogicalAddress();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         mNativeWrapper.clearResultMessages();
@@ -1108,7 +1106,11 @@
                 HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
                 HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW);
         mPowerManager.setInteractive(true);
-        HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+        HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress,
+                mPlaybackPhysicalAddress);
+        assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(message))
+                .isEqualTo(Constants.HANDLED);
+        message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
         assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(message))
                 .isEqualTo(Constants.HANDLED);
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 8112ca8..7a2a583 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -128,7 +128,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -165,8 +166,6 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -174,7 +173,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false);
@@ -185,11 +183,12 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTvPhysicalAddress = 0x0000;
         mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
+        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         for (String sad : SADS_NOT_TO_QUERY) {
             mHdmiControlService.getHdmiCecConfig().setIntValue(
                     sad, HdmiControlManager.QUERY_SAD_DISABLED);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index b94deed..a08e398 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -16,7 +16,6 @@
 package com.android.server.hdmi;
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -25,6 +24,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.os.Looper;
 import android.os.test.TestLooper;
@@ -40,7 +40,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 @SmallTest
@@ -57,7 +56,6 @@
     private FakeNativeWrapper mNativeWrapper;
     private FakePowerManagerWrapper mPowerManager;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiControlService mHdmiControlService;
     private HdmiCecLocalDevicePlayback mHdmiCecLocalDevicePlayback;
 
@@ -66,7 +64,8 @@
         Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(contextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             boolean isControlEnabled() {
@@ -90,9 +89,6 @@
         };
         mHdmiControlService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
 
-        mHdmiCecLocalDevicePlayback = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        mHdmiCecLocalDevicePlayback.init();
         mHdmiControlService.setIoLooper(myLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(contextSpy));
         mNativeWrapper = new FakeNativeWrapper();
@@ -100,7 +96,6 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false);
@@ -111,10 +106,9 @@
         mPowerManager = new FakePowerManagerWrapper(contextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.getHdmiCecNetwork().initPortInfo();
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x2000);
         mTestLooper.dispatchAll();
-
+        mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
         mHdmiCecPowerStatusController = new HdmiCecPowerStatusController(mHdmiControlService);
         mNativeWrapper.clearResultMessages();
     }
@@ -254,7 +248,6 @@
     private void setCecVersion(int version) {
         mHdmiControlService.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION, version);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 674e471..1b867be 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -39,6 +39,7 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
@@ -61,7 +62,6 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Optional;
 
 /**
@@ -84,14 +84,17 @@
     private TestLooper mTestLooper = new TestLooper();
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private HdmiPortInfo[] mHdmiPortInfo;
+    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
 
     @Before
     public void setUp() throws Exception {
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
+        mLocalDeviceTypes.add(DEVICE_AUDIO_SYSTEM);
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, mLocalDeviceTypes,
                 new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
@@ -228,8 +231,6 @@
 
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
         mNativeWrapper.clearResultMessages();
-
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         assertThat(mHdmiControlServiceSpy.getInitialPowerStatus()).isEqualTo(
@@ -461,7 +462,6 @@
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_1_4_B);
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveFeatures(Constants.ADDR_TV,
@@ -480,7 +480,6 @@
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveFeatures(Constants.ADDR_TV,
@@ -502,7 +501,6 @@
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_1_4_B);
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         HdmiCecMessage reportFeatures = ReportFeaturesMessage.build(
@@ -519,7 +517,6 @@
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
                 HdmiControlManager.HDMI_CEC_VERSION_2_0);
         mHdmiControlServiceSpy.setControlEnabled(HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
         HdmiCecMessage reportFeatures = ReportFeaturesMessage.build(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 1fa3871..9b8cedf 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -88,7 +88,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
@@ -142,11 +143,7 @@
     public void succeedWithUnknownTvDevice() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
 
@@ -191,11 +188,7 @@
     public void succeedAfterGettingPowerStatusOn_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -244,11 +237,7 @@
     public void succeedAfterGettingTransientPowerStatus_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -310,11 +299,7 @@
     public void timeOut_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -359,11 +344,7 @@
     @Test
     public void succeedIfPowerStatusOn_Cec20() throws Exception {
         setUp(true);
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -399,11 +380,8 @@
     @Test
     public void succeedIfPowerStatusUnknown_Cec20() throws Exception {
         setUp(true);
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -453,11 +431,7 @@
     @Test
     public void succeedIfPowerStatusStandby_Cec20() throws Exception {
         setUp(true);
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
@@ -510,11 +484,6 @@
 
         assertThat(mHdmiControlService.isAddressAllocated()).isFalse();
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-
         TestCallback callback = new TestCallback();
 
         mHdmiControlService.oneTouchPlay(callback);
@@ -524,9 +493,8 @@
         mNativeWrapper.clearResultMessages();
 
         setHdmiControlEnabled(true);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
-
         mTestLooper.dispatchAll();
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
 
         HdmiCecMessage reportPowerStatusMessage =
                 HdmiCecMessageBuilder.buildReportPowerStatus(
@@ -554,12 +522,7 @@
     public void succeedWithAddressAllocated_Cec14b() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
         assertThat(mHdmiControlService.isAddressAllocated()).isTrue();
 
@@ -632,11 +595,7 @@
     public void noWakeUpOnReportPowerStatus() throws Exception {
         setUp(true);
 
-        HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
-                mHdmiControlService);
-        playbackDevice.init();
-        mLocalDevices.add(playbackDevice);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        HdmiCecLocalDevicePlayback playbackDevice = mHdmiControlService.playback();
         mTestLooper.dispatchAll();
 
         mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index e5058be..f72ac71 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -20,7 +20,6 @@
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -44,7 +43,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 
@@ -60,7 +58,6 @@
     private FakePowerManagerWrapper mPowerManager;
 
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPhysicalAddress;
     private HdmiCecLocalDeviceTv mTvDevice;
 
@@ -101,9 +98,6 @@
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mTvDevice = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mTvDevice.init();
-        mLocalDevices.add(mTvDevice);
         mTestLooper.dispatchAll();
         HdmiPortInfo[] hdmiPortInfo = new HdmiPortInfo[2];
         hdmiPortInfo[0] =
@@ -117,8 +111,8 @@
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x0000;
         mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
+        mTvDevice = mHdmiControlService.tv();
         mNativeWrapper.clearResultMessages();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index c2519caa..c07d4be 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -18,12 +18,12 @@
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.os.Looper;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -69,7 +69,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mTvLogicalAddress;
     private List<byte[]> mSupportedSads;
     private RequestSadCallback mCallback =
@@ -97,7 +96,7 @@
         mMyLooper = mTestLooper.getLooper();
 
         mHdmiControlService =
-                new HdmiControlService(context, Collections.emptyList(),
+                new HdmiControlService(context, Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
                         new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
@@ -115,8 +114,6 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mNativeWrapper = new FakeNativeWrapper();
@@ -124,14 +121,13 @@
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
         mNativeWrapper.clearResultMessages();
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 566a7e0..f5bf30b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -25,7 +25,6 @@
 import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
 import static com.android.server.hdmi.Constants.MESSAGE_ACTIVE_SOURCE;
 import static com.android.server.hdmi.Constants.MESSAGE_ROUTING_INFORMATION;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTING_INFORMATION;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -134,7 +133,6 @@
     private FakePowerManagerWrapper mPowerManager;
     private Looper mMyLooper;
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
 
     private static RoutingControlAction createRoutingControlAction(HdmiCecLocalDeviceTv localDevice,
             TestInputSelectCallback callback) {
@@ -150,7 +148,8 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -172,15 +171,12 @@
                     }
                 };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(mMyLooper);
         mNativeWrapper = new FakeNativeWrapper();
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, PHYSICAL_ADDRESS_AVR,
@@ -190,9 +186,9 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mNativeWrapper.clearResultMessages();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(DEVICE_INFO_AVR);
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index dadf815..e3c8939 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -21,7 +21,6 @@
 import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -67,7 +66,6 @@
     private Context mContextSpy;
     private TestLooper mTestLooper = new TestLooper();
     private int mPhysicalAddress = 0x1100;
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPlaybackLogicalAddress;
 
     private TestCallback mTestCallback;
@@ -82,7 +80,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_PLAYBACK),
                 new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
@@ -104,21 +103,16 @@
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
 
-        mPlaybackDevice = new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy);
-        mPlaybackDevice.init();
-        mLocalDevices.add(mPlaybackDevice);
-
-        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
         mTestLooper.dispatchAll();
 
+        mPlaybackDevice = mHdmiControlServiceSpy.playback();
         mPlaybackLogicalAddress = mPlaybackDevice.getDeviceInfo().getLogicalAddress();
 
         // Setup specific to these tests
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
                 Constants.ADDR_TV, 0x0000, HdmiDeviceInfo.DEVICE_TV));
         mTestLooper.dispatchAll();
-
         mTestCallback = new TestCallback();
         mAction = new SetAudioVolumeLevelDiscoveryAction(mPlaybackDevice,
                 Constants.ADDR_TV, mTestCallback);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index 1644252..e7557fe 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -19,7 +19,6 @@
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
-import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -28,6 +27,7 @@
 
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
 import android.media.AudioManager;
 import android.os.Looper;
@@ -42,7 +42,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-import java.util.ArrayList;
 import java.util.Collections;
 
 /**
@@ -61,7 +60,6 @@
     private HdmiCecLocalDeviceTv mHdmiCecLocalDeviceTv;
 
     private TestLooper mTestLooper = new TestLooper();
-    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
     private int mPhysicalAddress;
 
     @Before
@@ -70,7 +68,8 @@
 
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+        mHdmiControlService = new HdmiControlService(mContextSpy,
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
                 new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
@@ -94,15 +93,12 @@
             }
         };
 
-        mHdmiCecLocalDeviceTv = new HdmiCecLocalDeviceTv(mHdmiControlService);
-        mHdmiCecLocalDeviceTv.init();
         mHdmiControlService.setIoLooper(myLooper);
         mNativeWrapper = new FakeNativeWrapper();
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
-        mLocalDevices.add(mHdmiCecLocalDeviceTv);
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
                 new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false);
@@ -113,10 +109,10 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mPhysicalAddress = 0x0000;
         mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
+        mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mPhysicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
         mNativeWrapper.clearResultMessages();
     }
diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
index 65076a3..c68db34 100644
--- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.ContextWrapper
 import android.hardware.BatteryState.STATUS_CHARGING
+import android.hardware.BatteryState.STATUS_DISCHARGING
 import android.hardware.BatteryState.STATUS_FULL
 import android.hardware.BatteryState.STATUS_UNKNOWN
 import android.hardware.input.IInputDeviceBatteryListener
@@ -32,6 +33,7 @@
 import android.platform.test.annotations.Presubmit
 import android.view.InputDevice
 import androidx.test.InstrumentationRegistry
+import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
 import com.android.server.input.BatteryController.UEventManager
 import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
 import org.hamcrest.Description
@@ -42,6 +44,8 @@
 import org.hamcrest.core.IsEqual.equalTo
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
@@ -63,14 +67,20 @@
 import org.mockito.junit.MockitoJUnit
 import org.mockito.verification.VerificationMode
 
-private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice =
+private fun createInputDevice(
+    deviceId: Int,
+    hasBattery: Boolean = true,
+    supportsUsi: Boolean = false,
+    generation: Int = -1,
+): InputDevice =
     InputDevice.Builder()
         .setId(deviceId)
         .setName("Device $deviceId")
         .setDescriptor("descriptor $deviceId")
         .setExternal(true)
         .setHasBattery(hasBattery)
-        .setGeneration(0)
+        .setSupportsUsi(supportsUsi)
+        .setGeneration(generation)
         .build()
 
 // Returns a matcher that helps match member variables of a class.
@@ -118,7 +128,10 @@
     return Matchers.allOf(batteryStateMatchers)
 }
 
-// Helper used to verify interactions with a mocked battery listener.
+private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> =
+    matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN)
+
+// Helpers used to verify interactions with a mocked battery listener.
 private fun IInputDeviceBatteryListener.verifyNotified(
     deviceId: Int,
     mode: VerificationMode = times(1),
@@ -127,8 +140,21 @@
     capacity: Float? = null,
     eventTime: Long? = null
 ) {
-    verify(this, mode).onBatteryStateChanged(
-        MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime)))
+    verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode)
+}
+
+private fun IInputDeviceBatteryListener.verifyNotified(
+    matcher: Matcher<IInputDeviceBatteryState>,
+    mode: VerificationMode = times(1)
+) {
+    verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher))
+}
+
+private fun createMockListener(): IInputDeviceBatteryListener {
+    val listener = mock(IInputDeviceBatteryListener::class.java)
+    val binder = mock(Binder::class.java)
+    `when`(listener.asBinder()).thenReturn(binder)
+    return listener
 }
 
 /**
@@ -143,6 +169,8 @@
         const val PID = 42
         const val DEVICE_ID = 13
         const val SECOND_DEVICE_ID = 11
+        const val USI_DEVICE_ID = 101
+        const val SECOND_USI_DEVICE_ID = 102
         const val TIMESTAMP = 123456789L
     }
 
@@ -168,10 +196,11 @@
         testLooper = TestLooper()
         val inputManager = InputManager.resetInstance(iInputManager)
         `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
-        `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID))
-        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID))
-        `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
-            .thenReturn(createInputDevice(SECOND_DEVICE_ID))
+        `when`(iInputManager.inputDeviceIds).then {
+            deviceGenerationMap.keys.toIntArray()
+        }
+        addInputDevice(DEVICE_ID)
+        addInputDevice(SECOND_DEVICE_ID)
 
         batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
         batteryController.systemRunning()
@@ -180,10 +209,30 @@
         devicesChangedListener = listenerCaptor.value
     }
 
-    private fun notifyDeviceChanged(deviceId: Int) {
-        deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1
+    private fun notifyDeviceChanged(
+            deviceId: Int,
+        hasBattery: Boolean = true,
+        supportsUsi: Boolean = false
+    ) {
+        val generation = deviceGenerationMap[deviceId]?.plus(1)
+            ?: throw IllegalArgumentException("Device $deviceId was never added!")
+        deviceGenerationMap[deviceId] = generation
+
+        `when`(iInputManager.getInputDevice(deviceId))
+            .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation))
         val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) }
-        devicesChangedListener.onInputDevicesChanged(list.toIntArray())
+        if (::devicesChangedListener.isInitialized) {
+            devicesChangedListener.onInputDevicesChanged(list.toIntArray())
+        }
+    }
+
+    private fun addInputDevice(
+            deviceId: Int,
+        hasBattery: Boolean = true,
+        supportsUsi: Boolean = false
+    ) {
+        deviceGenerationMap[deviceId] = 0
+        notifyDeviceChanged(deviceId, hasBattery, supportsUsi)
     }
 
     @After
@@ -191,13 +240,6 @@
         InputManager.clearInstance()
     }
 
-    private fun createMockListener(): IInputDeviceBatteryListener {
-        val listener = mock(IInputDeviceBatteryListener::class.java)
-        val binder = mock(Binder::class.java)
-        `when`(listener.asBinder()).thenReturn(binder)
-        return listener
-    }
-
     @Test
     fun testRegisterAndUnregisterBinderLifecycle() {
         val listener = createMockListener()
@@ -303,19 +345,14 @@
         listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)
 
         // If the battery presence for the InputDevice changes, the listener is notified.
-        `when`(iInputManager.getInputDevice(DEVICE_ID))
-            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false))
-        notifyDeviceChanged(DEVICE_ID)
+        notifyDeviceChanged(DEVICE_ID, hasBattery = false)
         testLooper.dispatchNext()
-        listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN,
-            capacity = Float.NaN)
+        listener.verifyNotified(isInvalidBatteryState(DEVICE_ID))
         // Since the battery is no longer present, the UEventListener should be removed.
         verify(uEventManager).removeListener(uEventListener.value)
 
         // If the battery becomes present again, the listener is notified.
-        `when`(iInputManager.getInputDevice(DEVICE_ID))
-            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true))
-        notifyDeviceChanged(DEVICE_ID)
+        notifyDeviceChanged(DEVICE_ID, hasBattery = true)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING,
             capacity = 0.78f)
@@ -340,9 +377,17 @@
 
         // Move the time forward so that the polling period has elapsed.
         // The listener should be notified.
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, capacity = 0.80f)
+
+        // Move the time forward so that another polling period has elapsed.
+        // The battery should still be polled, but there is no change so listeners are not notified.
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
+        testLooper.dispatchNext()
+        listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f)
     }
 
     @Test
@@ -357,7 +402,8 @@
         // The battery state changed, but we should not be polling for battery changes when the
         // device is not interactive.
         `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchAll()
         listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f)
 
@@ -368,7 +414,8 @@
 
         // Ensure that we continue to poll for battery changes.
         `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90)
-        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
         testLooper.dispatchNext()
         listener.verifyNotified(DEVICE_ID, capacity = 0.90f)
     }
@@ -398,4 +445,93 @@
             matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f))
         listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)
     }
+
+    @Test
+    fun testUsiDeviceIsMonitoredPersistently() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+
+        // Even though there is no listener added for this device, it is being monitored.
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // Add and remove a listener for the device.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID)
+
+        // The device is still being monitored.
+        verify(uEventManager, never()).removeListener(uEventListener.value)
+    }
+
+    @Test
+    fun testNoPollingWhenUsiDevicesAreMonitored() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2")
+        addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
+
+        // Add a listener.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+
+        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
+        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
+    }
+
+    @Test
+    fun testExpectedFlowForUsiBattery() {
+        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
+        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING)
+        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78)
+
+        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
+        testLooper.dispatchNext()
+        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
+        verify(uEventManager)
+            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
+
+        // A USI device's battery state is not valid until the first UEvent notification.
+        // Add a listener, and ensure it is notified that the battery state is not present.
+        val listener = createMockListener()
+        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
+        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
+
+        // Ensure that querying for battery state also returns the same invalid result.
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+
+        // There is a UEvent signaling a battery change. The battery state is now valid.
+        uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
+        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))
+
+        // There is another UEvent notification. The battery state is now updated.
+        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(64)
+        uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1)
+        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
+
+        // The battery state is still valid after a millisecond.
+        testLooper.moveTimeForward(1)
+        testLooper.dispatchAll()
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
+
+        // The battery is no longer present after the timeout expires.
+        testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1)
+        testLooper.dispatchNext()
+        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2))
+        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
+            isInvalidBatteryState(USI_DEVICE_ID))
+    }
 }
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 c735d18..8f0fb0b 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -45,14 +45,14 @@
 import android.os.RemoteException;
 import android.os.SimpleClock;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
index 853eea1..ee97466 100644
--- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
@@ -44,13 +44,13 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.AtomicFile;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import org.junit.After;
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
new file mode 100644
index 0000000..fb1a8f8
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.contexthub;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyBoolean;
+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.location.ContextHubInfo;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
+
+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 org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class ContextHubServiceTest {
+    private static final int CONTEXT_HUB_ID = 3;
+    private static final String CONTEXT_HUB_STRING = "Context Hub Info Test";
+
+    private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+    @Mock private IContextHubWrapper mMockContextHubWrapper;
+    @Mock private ContextHubInfo mMockContextHubInfo;
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Before
+    public void setUp() throws RemoteException {
+        Pair<List<ContextHubInfo>, List<String>> hubInfo =
+                new Pair<>(Arrays.asList(mMockContextHubInfo), Arrays.asList(""));
+        when(mMockContextHubInfo.getId()).thenReturn(CONTEXT_HUB_ID);
+        when(mMockContextHubInfo.toString()).thenReturn(CONTEXT_HUB_STRING);
+        when(mMockContextHubWrapper.getHubs()).thenReturn(hubInfo);
+
+        when(mMockContextHubWrapper.supportsLocationSettingNotifications())
+                .thenReturn(true);
+        when(mMockContextHubWrapper.supportsWifiSettingNotifications()).thenReturn(true);
+        when(mMockContextHubWrapper.supportsAirplaneModeSettingNotifications())
+                .thenReturn(true);
+        when(mMockContextHubWrapper.supportsMicrophoneSettingNotifications())
+                .thenReturn(true);
+        when(mMockContextHubWrapper.supportsBtSettingNotifications()).thenReturn(true);
+    }
+
+// TODO (b/254290317): These existing tests are to setup the testing infra for the ContextHub
+//                     service and verify the constructor correctly registers a context hub.
+//                     We need to augment these tests to cover the full behavior of the
+//                     ContextHub service
+
+    @Test
+    public void testConstructorRegistersContextHub() throws RemoteException {
+        ContextHubService service = new ContextHubService(mContext, mMockContextHubWrapper);
+        assertThat(service.getContextHubInfo(CONTEXT_HUB_ID)).isEqualTo(mMockContextHubInfo);
+    }
+
+    @Test
+    public void testConstructorRegistersNotifications() {
+        new ContextHubService(mContext, mMockContextHubWrapper);
+        verify(mMockContextHubWrapper).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onWifiSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onWifiScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onWifiMainSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onMicrophoneSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onBtScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper).onBtMainSettingChanged(anyBoolean());
+    }
+
+    @Test
+    public void testConstructorRegistersNotificationsAndHandlesSettings() {
+        when(mMockContextHubWrapper.supportsLocationSettingNotifications())
+                .thenReturn(false);
+        when(mMockContextHubWrapper.supportsWifiSettingNotifications()).thenReturn(false);
+        when(mMockContextHubWrapper.supportsAirplaneModeSettingNotifications())
+                .thenReturn(false);
+        when(mMockContextHubWrapper.supportsMicrophoneSettingNotifications())
+                .thenReturn(false);
+        when(mMockContextHubWrapper.supportsBtSettingNotifications()).thenReturn(false);
+
+        new ContextHubService(mContext, mMockContextHubWrapper);
+        verify(mMockContextHubWrapper, never()).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onWifiSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onWifiScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onWifiMainSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onAirplaneModeSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onMicrophoneSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onBtScanningSettingChanged(anyBoolean());
+        verify(mMockContextHubWrapper, never()).onBtMainSettingChanged(anyBoolean());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 3bcde6a..b7f90d4 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -113,8 +113,10 @@
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.app.usage.UsageStatsManagerInternal;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageInfo;
@@ -134,6 +136,7 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.net.wifi.WifiInfo;
 import android.os.Binder;
+import android.os.Build;
 import android.os.Handler;
 import android.os.INetworkManagementService;
 import android.os.PersistableBundle;
@@ -152,6 +155,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.DataUnit;
 import android.util.Log;
 import android.util.Pair;
@@ -171,11 +175,12 @@
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.usage.AppStandbyInternal;
 
-import libcore.io.Streams;
-
 import com.google.common.util.concurrent.AbstractFuture;
 
+import libcore.io.Streams;
+
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -286,6 +291,8 @@
     private NetworkPolicyListenerAnswer mPolicyListener;
     private NetworkPolicyManagerService mService;
 
+    private final ArraySet<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>();
+
     /**
      * In some of the tests while initializing NetworkPolicyManagerService,
      * ACTION_RESTRICT_BACKGROUND_CHANGED is broadcasted. This is for capturing that broadcast.
@@ -437,6 +444,21 @@
             public void enforceCallingOrSelfPermission(String permission, String message) {
                 // Assume that we're AID_SYSTEM
             }
+
+            @Override
+            public Intent registerReceiver(BroadcastReceiver receiver,
+                    IntentFilter filter, String broadcastPermission, Handler scheduler) {
+                mRegisteredReceivers.add(receiver);
+                return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
+            }
+
+            @Override
+            public Intent registerReceiverForAllUsers(BroadcastReceiver receiver,
+                    IntentFilter filter, String broadcastPermission, Handler scheduler) {
+                mRegisteredReceivers.add(receiver);
+                return super.registerReceiverForAllUsers(receiver, filter, broadcastPermission,
+                        scheduler);
+            }
         };
 
         setNetpolicyXml(context);
@@ -557,6 +579,13 @@
         RecurrenceRule.sClock = Clock.systemDefaultZone();
     }
 
+    @After
+    public void unregisterReceivers() throws Exception {
+        for (BroadcastReceiver receiver : mRegisteredReceivers) {
+            mServiceContext.unregisterReceiver(receiver);
+        }
+    }
+
     @Test
     public void testTurnRestrictBackgroundOn() throws Exception {
         assertRestrictBackgroundOff();
@@ -2033,6 +2062,9 @@
 
     @Test
     public void testNormalizeTemplate_duplicatedMergedImsiList() {
+        // This test leads to a Log.wtf, so skip it on eng builds. Otherwise, Log.wtf() would
+        // result in this process getting killed.
+        Assume.assumeFalse(Build.IS_ENG);
         final NetworkTemplate template = new NetworkTemplate.Builder(MATCH_CARRIER)
                 .setSubscriberIds(Set.of(TEST_IMSI)).build();
         final String[] mergedImsiGroup = new String[] {TEST_IMSI, TEST_IMSI};
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
index 94e67d1..3f55f1b 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
@@ -16,16 +16,16 @@
 
 package com.android.server.om;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -43,6 +43,9 @@
     private static final String OVERLAY2 = OVERLAY + "2";
     private static final OverlayIdentifier IDENTIFIER2 = new OverlayIdentifier(OVERLAY2);
 
+    @Rule
+    public final Expect expect = Expect.create();
+
     @Test
     public void alwaysInitializeAllPackages() {
         final OverlayManagerServiceImpl impl = getImpl();
@@ -51,13 +54,11 @@
         addPackage(target(otherTarget), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER),
-                        new PackageAndUser(otherTarget, USER),
-                        new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        // The result should be the same for every time
+        assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
+        assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
     }
 
     @Test
@@ -66,29 +67,31 @@
         addPackage(target(TARGET), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertFalse(o1.isEnabled());
-        assertFalse(o1.isMutable);
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.isEnabled()).isFalse();
+        expect.that(o1.isMutable).isFalse();
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o2);
-        assertTrue(o2.isEnabled());
-        assertFalse(o2.isMutable);
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.isEnabled()).isTrue();
+        expect.that(o2.isMutable).isFalse();
 
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertFalse(o3.isEnabled());
-        assertFalse(o3.isMutable);
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.isEnabled()).isFalse();
+        expect.that(o3.isMutable).isFalse();
     }
 
     @Test
@@ -98,28 +101,30 @@
         addPackage(overlay(OVERLAY, TARGET), USER);
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertFalse(o1.isEnabled());
-        assertTrue(o1.isMutable);
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.isEnabled()).isFalse();
+        expect.that(o1.isMutable).isTrue();
 
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o2);
-        assertFalse(o2.isEnabled());
-        assertTrue(o2.isMutable);
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.isEnabled()).isFalse();
+        expect.that(o2.isMutable).isTrue();
 
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertFalse(o3.isEnabled());
-        assertTrue(o3.isMutable);
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.isEnabled()).isFalse();
+        expect.that(o3.isMutable).isTrue();
     }
 
     @Test
@@ -128,17 +133,17 @@
         addPackage(target(TARGET), USER);
         addPackage(overlay(OVERLAY, TARGET), USER);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
         final Consumer<ConfigState> setOverlay = (state -> {
             configureSystemOverlay(OVERLAY, state, 0 /* priority */);
-            assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+            expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
             final OverlayInfo o = impl.getOverlayInfo(IDENTIFIER, USER);
-            assertNotNull(o);
-            assertEquals(o.isEnabled(), state == ConfigState.IMMUTABLE_ENABLED
+            expect.that(o).isNotNull();
+            assertThat(expect.hasFailures()).isFalse();
+            expect.that(o.isEnabled()).isEqualTo(state == ConfigState.IMMUTABLE_ENABLED
                     || state == ConfigState.MUTABLE_ENABLED);
-            assertEquals(o.isMutable, state == ConfigState.MUTABLE_DISABLED
+            expect.that(o.isMutable).isEqualTo(state == ConfigState.MUTABLE_DISABLED
                     || state == ConfigState.MUTABLE_ENABLED);
         });
 
@@ -180,20 +185,20 @@
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER),
-                        new PackageAndUser(OVERLAY2, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertEquals(0, o1.priority);
-        assertFalse(o1.isEnabled());
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.priority).isEqualTo(0);
+        expect.that(o1.isEnabled()).isFalse();
 
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o2);
-        assertEquals(1, o2.priority);
-        assertFalse(o2.isEnabled());
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.priority).isEqualTo(1);
+        expect.that(o2.isEnabled()).isFalse();
 
         // Overlay priority changing between reboots should not affect enable state of mutable
         // overlays.
@@ -202,16 +207,18 @@
         // Reorder the overlays
         configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertEquals(1, o3.priority);
-        assertTrue(o3.isEnabled());
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.priority).isEqualTo(1);
+        expect.that(o3.isEnabled()).isTrue();
 
         final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o4);
-        assertEquals(0, o4.priority);
-        assertFalse(o4.isEnabled());
+        expect.that(o4).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o4.priority).isEqualTo(0);
+        expect.that(o4.isEnabled()).isFalse();
     }
 
     @Test
@@ -223,33 +230,35 @@
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
 
-        final Set<PackageAndUser> allPackages =
-                Set.of(new PackageAndUser(TARGET, USER), new PackageAndUser(OVERLAY, USER),
-                        new PackageAndUser(OVERLAY2, USER));
+        final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
 
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o1);
-        assertEquals(0, o1.priority);
-        assertTrue(o1.isEnabled());
+        expect.that(o1).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o1.priority).isEqualTo(0);
+        expect.that(o1.isEnabled()).isTrue();
 
         final OverlayInfo o2 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o2);
-        assertEquals(1, o2.priority);
-        assertTrue(o2.isEnabled());
+        expect.that(o2).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o2.priority).isEqualTo(1);
+        expect.that(o2.isEnabled()).isTrue();
 
         // Reorder the overlays
         configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
         configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
-        assertEquals(allPackages, impl.updateOverlaysForUser(USER));
+        expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
         final OverlayInfo o3 = impl.getOverlayInfo(IDENTIFIER, USER);
-        assertNotNull(o3);
-        assertEquals(1, o3.priority);
-        assertTrue(o3.isEnabled());
+        expect.that(o3).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o3.priority).isEqualTo(1);
+        expect.that(o3.isEnabled()).isTrue();
 
         final OverlayInfo o4 = impl.getOverlayInfo(IDENTIFIER2, USER);
-        assertNotNull(o4);
-        assertEquals(0, o4.priority);
-        assertTrue(o4.isEnabled());
+        expect.that(o4).isNotNull();
+        assertThat(expect.hasFailures()).isFalse();
+        expect.that(o4.priority).isEqualTo(0);
+        expect.that(o4.isEnabled()).isTrue();
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
index 0a26f27..3f7eac7 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
@@ -29,12 +29,13 @@
 import android.content.om.OverlayIdentifier;
 import android.content.om.OverlayInfo;
 import android.text.TextUtils;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.annotation.NonNull;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
index a545b1f..648f895 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java
@@ -32,13 +32,13 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.AtomicFile;
 import android.util.Slog;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.BackgroundThread;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
index 68310f4..9ce99d6 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 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.assertSame;
@@ -127,6 +128,7 @@
     private static final String TEST_APP3_APK = "PackageParserTestApp3.apk";
     private static final String TEST_APP4_APK = "PackageParserTestApp4.apk";
     private static final String TEST_APP5_APK = "PackageParserTestApp5.apk";
+    private static final String TEST_APP6_APK = "PackageParserTestApp6.apk";
     private static final String PACKAGE_NAME = "com.android.servicestests.apps.packageparserapp";
 
     @Before
@@ -331,6 +333,46 @@
         }
     }
 
+    @Test
+    public void testParseActivityTargetDisplayCategoryValid() throws Exception {
+        final File testFile = extractFile(TEST_APP4_APK);
+        String actualDisplayCategory = null;
+        try {
+            final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            final List<ParsedActivity> activities = pkg.getActivities();
+            for (ParsedActivity activity : activities) {
+                if ((PACKAGE_NAME + ".MyActivity").equals(activity.getName())) {
+                    actualDisplayCategory = activity.getTargetDisplayCategory();
+                }
+            }
+        } finally {
+            testFile.delete();
+        }
+        assertEquals("automotive", actualDisplayCategory);
+    }
+
+    @Test
+    public void testParseActivityTargetDisplayCategoryInvalid() throws Exception {
+        final File testFile = extractFile(TEST_APP6_APK);
+        String actualDisplayCategory = null;
+        try {
+            final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            final List<ParsedActivity> activities = pkg.getActivities();
+            for (ParsedActivity activity : activities) {
+                if ((PACKAGE_NAME + ".MyActivity").equals(activity.getName())) {
+                    actualDisplayCategory = activity.getTargetDisplayCategory();
+                }
+            }
+        } catch (PackageManagerException e) {
+            assertThat(e.getMessage()).contains(
+                    "targetDisplayCategory attribute can only consists"
+                            + " of alphanumeric characters, '_', and '.'");
+        } finally {
+            testFile.delete();
+        }
+        assertNotEquals("$automotive", actualDisplayCategory);
+    }
+
     private static final int PROPERTY_TYPE_BOOLEAN = 1;
     private static final int PROPERTY_TYPE_FLOAT = 2;
     private static final int PROPERTY_TYPE_INTEGER = 3;
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java
index 7e4474f..47f75a5 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageSignaturesTest.java
@@ -25,13 +25,13 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.platform.test.annotations.Presubmit;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.HexDump;
+import com.android.modules.utils.TypedXmlPullParser;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 867890f..96c0d0a 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -98,13 +98,13 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.Log;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.frameworks.servicestests.R;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.pm.ShortcutService.ConfigConstants;
 import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
 import com.android.server.pm.ShortcutUser.PackageWithUser;
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 13a7a3e..8efcc41 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -23,13 +23,14 @@
 import android.content.pm.UserProperties;
 import android.os.Parcel;
 import android.platform.test.annotations.Presubmit;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.MediumTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index fe4db3a..db2630e2 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -87,7 +87,6 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
-import com.android.server.compat.PlatformCompat;
 import com.android.server.lights.LightsManager;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.power.PowerManagerService.BatteryReceiver;
@@ -147,7 +146,6 @@
     @Mock private SystemPropertiesWrapper mSystemPropertiesMock;
     @Mock private AppOpsManager mAppOpsManagerMock;
     @Mock private LowPowerStandbyController mLowPowerStandbyControllerMock;
-    @Mock private PlatformCompat mPlatformCompat;
 
     @Mock
     private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@@ -321,11 +319,6 @@
             AppOpsManager createAppOpsManager(Context context) {
                 return mAppOpsManagerMock;
             }
-
-            @Override
-            PlatformCompat createPlatformCompat(Context context) {
-                return mPlatformCompat;
-            }
         });
         return mService;
     }
@@ -505,9 +498,6 @@
         String packageName = "pkg.name";
         when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON,
                 Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED);
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(true);
         when(mContextSpy.checkCallingOrSelfPermission(
                 android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
                 PackageManager.PERMISSION_GRANTED);
@@ -532,23 +522,6 @@
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
         assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
-
-        // Verify that on older platforms only the appOp is necessary and the permission isn't
-        // checked
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(false);
-        when(mContextSpy.checkCallingOrSelfPermission(
-                android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
-                PackageManager.PERMISSION_DENIED);
-        forceSleep();
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
-
-        flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
-        mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
-                null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
-        mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
     }
 
     @Test
@@ -568,7 +541,7 @@
         int flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
         mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         } else {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
@@ -577,9 +550,6 @@
 
         when(mAppOpsManagerMock.checkOpNoThrow(AppOpsManager.OP_TURN_SCREEN_ON,
                 Binder.getCallingUid(), packageName)).thenReturn(MODE_ALLOWED);
-        when(mPlatformCompat.isChangeEnabledByPackageName(
-                eq(PowerManagerService.REQUIRE_TURN_SCREEN_ON_PERMISSION), anyString(),
-                anyInt())).thenReturn(true);
         when(mContextSpy.checkCallingOrSelfPermission(
                 android.Manifest.permission.TURN_SCREEN_ON)).thenReturn(
                 PackageManager.PERMISSION_DENIED);
@@ -589,7 +559,7 @@
         flags = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP;
         mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
                 null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, null);
-        if (PowerProperties.permissionless_turn_screen_on().orElse(true)) {
+        if (PowerProperties.permissionless_turn_screen_on().orElse(false)) {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         } else {
             assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
index 1049274..970020f 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java
@@ -27,13 +27,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.os.PowerProfile;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index 067f4e2..e603ea5 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -37,13 +37,14 @@
 import android.os.Parcel;
 import android.os.UidBatteryConsumer;
 import android.os.UserBatteryConsumer;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
diff --git a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java
index 7ac4938..9c61d95 100644
--- a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java
@@ -21,10 +21,10 @@
 import android.app.usage.CacheQuotaHint;
 import android.test.AndroidTestCase;
 import android.util.Pair;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.FastXmlSerializer;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
index 52e9d3a..34d0082 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
@@ -17,6 +17,7 @@
 package com.android.server.timezonedetector.location;
 
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 
 /**
  * Fake implementation of {@link TimeZoneProviderEventPreProcessor} which assumes that all events
@@ -31,7 +32,8 @@
     public TimeZoneProviderEvent preProcess(TimeZoneProviderEvent timeZoneProviderEvent) {
         if (mIsUncertain) {
             return TimeZoneProviderEvent.createUncertainEvent(
-                    timeZoneProviderEvent.getCreationElapsedMillis());
+                    timeZoneProviderEvent.getCreationElapsedMillis(),
+                    TimeZoneProviderStatus.UNKNOWN);
         }
         return timeZoneProviderEvent;
     }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java
index 0257ce0..ed426cd 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderControllerTest.java
@@ -15,6 +15,12 @@
  */
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_NOT_APPLICABLE;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE;
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_UNKNOWN;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING;
+
 import static com.android.server.timezonedetector.ConfigurationInternal.DETECTION_MODE_MANUAL;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_PERM_FAILED;
@@ -48,6 +54,7 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.Presubmit;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
 
@@ -78,8 +85,15 @@
             createSuggestionEvent(asList("Europe/London"));
     private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 =
             createSuggestionEvent(asList("Europe/Paris"));
+    private static final TimeZoneProviderStatus UNCERTAIN_PROVIDER_STATUS =
+            new TimeZoneProviderStatus.Builder()
+                    .setLocationDetectionStatus(DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE)
+                    .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                    .setTimeZoneResolutionStatus(OPERATION_STATUS_UNKNOWN)
+                    .build();
     private static final TimeZoneProviderEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT =
-            TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_TIME_MILLIS);
+            TimeZoneProviderEvent.createUncertainEvent(
+                    ARBITRARY_TIME_MILLIS, UNCERTAIN_PROVIDER_STATUS);
     private static final TimeZoneProviderEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT =
             TimeZoneProviderEvent.createPermanentFailureEvent(ARBITRARY_TIME_MILLIS, "Test");
 
@@ -1390,12 +1404,17 @@
     }
 
     private static TimeZoneProviderEvent createSuggestionEvent(@NonNull List<String> timeZoneIds) {
+        TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_NOT_APPLICABLE)
+                .setConnectivityStatus(DEPENDENCY_STATUS_NOT_APPLICABLE)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING)
+                .build();
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
+                .setTimeZoneIds(timeZoneIds)
+                .build();
         return TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_TIME_MILLIS,
-                new TimeZoneProviderSuggestion.Builder()
-                        .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
-                        .setTimeZoneIds(timeZoneIds)
-                        .build());
+                ARBITRARY_TIME_MILLIS, suggestion, providerStatus);
     }
 
     private static void assertControllerState(LocationTimeZoneProviderController controller,
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
index cb2905d..8429fa4 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
@@ -33,6 +33,7 @@
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
 
@@ -120,8 +121,9 @@
                 .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
                 .setTimeZoneIds(Arrays.asList("Europe/London"))
                 .build();
+        TimeZoneProviderStatus providerStatus = TimeZoneProviderStatus.UNKNOWN;
         TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion);
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion, providerStatus);
         provider.simulateProviderEventReceived(event);
 
         currentState = assertAndReturnProviderState(
@@ -133,7 +135,8 @@
         mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN);
 
         // Simulate an uncertain event being received.
-        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS,
+                TimeZoneProviderStatus.UNKNOWN);
         provider.simulateProviderEventReceived(event);
 
         currentState = assertAndReturnProviderState(
@@ -193,12 +196,13 @@
                 .setTimeZoneIds(Arrays.asList("Europe/London"))
                 .build();
         TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion);
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion, TimeZoneProviderStatus.UNKNOWN);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_CERTAIN);
 
         // Simulate an uncertain event being received.
-        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS);
+        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS,
+                TimeZoneProviderStatus.UNKNOWN);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN);
 
@@ -235,8 +239,9 @@
                 .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
                 .setTimeZoneIds(invalidTimeZoneIds)
                 .build();
+        TimeZoneProviderStatus providerStatus = TimeZoneProviderStatus.UNKNOWN;
         TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion);
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion, providerStatus);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN);
     }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
index ab4fe29..c478604 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
@@ -16,10 +16,15 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderStatus.DEPENDENCY_STATUS_WORKING;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_FAILED;
+import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_WORKING;
+
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.platform.test.annotations.Presubmit;
 import android.service.timezone.TimeZoneProviderEvent;
+import android.service.timezone.TimeZoneProviderStatus;
 import android.service.timezone.TimeZoneProviderSuggestion;
 
 import org.junit.Test;
@@ -54,8 +59,14 @@
         for (String timeZone : nonExistingTimeZones) {
             TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone);
 
+            TimeZoneProviderStatus expectedProviderStatus =
+                    new TimeZoneProviderStatus.Builder(event.getTimeZoneProviderStatus())
+                            .setTimeZoneResolutionStatus(OPERATION_STATUS_FAILED)
+                            .build();
+
             TimeZoneProviderEvent expectedResultEvent =
-                    TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis());
+                    TimeZoneProviderEvent.createUncertainEvent(
+                            event.getCreationElapsedMillis(), expectedProviderStatus);
             assertWithMessage(timeZone + " is not a valid time zone")
                     .that(mPreProcessor.preProcess(event))
                     .isEqualTo(expectedResultEvent);
@@ -63,12 +74,17 @@
     }
 
     private static TimeZoneProviderEvent timeZoneProviderEvent(String... timeZoneIds) {
+        TimeZoneProviderStatus providerStatus = new TimeZoneProviderStatus.Builder()
+                .setLocationDetectionStatus(DEPENDENCY_STATUS_WORKING)
+                .setConnectivityStatus(DEPENDENCY_STATUS_WORKING)
+                .setTimeZoneResolutionStatus(OPERATION_STATUS_WORKING)
+                .build();
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setTimeZoneIds(Arrays.asList(timeZoneIds))
+                .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
+                .build();
         return TimeZoneProviderEvent.createSuggestionEvent(
-                ARBITRARY_TIME_MILLIS,
-                new TimeZoneProviderSuggestion.Builder()
-                        .setTimeZoneIds(Arrays.asList(timeZoneIds))
-                        .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
-                .build());
+                ARBITRARY_TIME_MILLIS, suggestion, providerStatus);
     }
 
 }
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
index 235849c..c484f45 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/FakeVibratorControllerProvider.java
@@ -53,7 +53,8 @@
 
     private boolean mIsAvailable = true;
     private boolean mIsInfoLoadSuccessful = true;
-    private long mLatency;
+    private long mOnLatency;
+    private long mOffLatency;
     private int mOffCount;
 
     private int mCapabilities;
@@ -97,7 +98,7 @@
         public long on(long milliseconds, long vibrationId) {
             recordEffectSegment(vibrationId, new StepSegment(VibrationEffect.DEFAULT_AMPLITUDE,
                     /* frequencyHz= */ 0, (int) milliseconds));
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(milliseconds, vibrationId);
             return milliseconds;
         }
@@ -105,12 +106,13 @@
         @Override
         public void off() {
             mOffCount++;
+            applyLatency(mOffLatency);
         }
 
         @Override
         public void setAmplitude(float amplitude) {
             mAmplitudes.add(amplitude);
-            applyLatency();
+            applyLatency(mOnLatency);
         }
 
         @Override
@@ -121,7 +123,7 @@
             }
             recordEffectSegment(vibrationId,
                     new PrebakedSegment((int) effect, false, (int) strength));
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(EFFECT_DURATION, vibrationId);
             return EFFECT_DURATION;
         }
@@ -141,7 +143,7 @@
                 duration += EFFECT_DURATION + primitive.getDelay();
                 recordEffectSegment(vibrationId, primitive);
             }
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(duration, vibrationId);
             return duration;
         }
@@ -154,7 +156,7 @@
                 recordEffectSegment(vibrationId, primitive);
             }
             recordBraking(vibrationId, braking);
-            applyLatency();
+            applyLatency(mOnLatency);
             scheduleListener(duration, vibrationId);
             return duration;
         }
@@ -193,10 +195,10 @@
             return mIsInfoLoadSuccessful;
         }
 
-        private void applyLatency() {
+        private void applyLatency(long latencyMillis) {
             try {
-                if (mLatency > 0) {
-                    Thread.sleep(mLatency);
+                if (latencyMillis > 0) {
+                    Thread.sleep(latencyMillis);
                 }
             } catch (InterruptedException e) {
             }
@@ -240,10 +242,15 @@
 
     /**
      * Sets the latency this controller should fake for turning the vibrator hardware on or setting
-     * it's vibration amplitude.
+     * the vibration amplitude.
      */
-    public void setLatency(long millis) {
-        mLatency = millis;
+    public void setOnLatency(long millis) {
+        mOnLatency = millis;
+    }
+
+    /** Sets the latency this controller should fake for turning the vibrator off. */
+    public void setOffLatency(long millis) {
+        mOffLatency = millis;
     }
 
     /** Set the capabilities of the fake vibrator hardware. */
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index a15e4b0..fc830a9 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -1159,7 +1159,7 @@
 
         // 25% of the first waveform step will be spent on the native on() call.
         // 25% of each waveform step will be spent on the native setAmplitude() call..
-        mVibratorProviders.get(VIBRATOR_ID).setLatency(stepDuration / 4);
+        mVibratorProviders.get(VIBRATOR_ID).setOnLatency(stepDuration / 4);
         mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
 
         int stepCount = totalDuration / stepDuration;
@@ -1190,7 +1190,7 @@
         fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
 
         long latency = 5_000; // 5s
-        fakeVibrator.setLatency(latency);
+        fakeVibrator.setOnLatency(latency);
 
         long vibrationId = 1;
         VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
@@ -1204,8 +1204,7 @@
         // fail at waitForCompletion(cancellingThread).
         Thread cancellingThread = new Thread(
                 () -> conductor.notifyCancelled(
-                        new Vibration.EndInfo(
-                                Vibration.Status.CANCELLED_BY_USER),
+                        new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER),
                         /* immediate= */ false));
         cancellingThread.start();
 
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index c46fecd..c83afb7 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -826,13 +826,40 @@
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
         // No segment played is the prebaked CLICK from the second vibration.
-        assertFalse(
-                mVibratorProviders.get(1).getAllEffectSegments().stream()
-                        .anyMatch(segment -> segment instanceof PrebakedSegment));
+        assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
         cancelVibrate(service);  // Clean up repeating effect.
     }
 
     @Test
+    public void vibrate_withOngoingRepeatingVibrationBeingCancelled_playsAfterPreviousIsCancelled()
+            throws Exception {
+        mockVibrators(1);
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1);
+        fakeVibrator.setOffLatency(50); // Add latency so cancellation is slow.
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK);
+        VibratorManagerService service = createSystemReadyService();
+
+        VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
+                new long[]{10, 10_000}, new int[]{255, 0}, 1);
+        vibrate(service, repeatingEffect, ALARM_ATTRS);
+
+        // VibrationThread will start this vibration async, wait until the off waveform step.
+        assertTrue(waitUntil(s -> fakeVibrator.getOffCount() > 0, service, TEST_TIMEOUT_MILLIS));
+
+        // Cancel vibration right before requesting a new one.
+        // This should trigger slow IVibrator.off before setting the vibration status to cancelled.
+        cancelVibrate(service);
+        vibrateAndWaitUntilFinished(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
+                ALARM_ATTRS);
+
+        // Check that second vibration was played.
+        assertTrue(fakeVibrator.getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
+    }
+
+    @Test
     public void vibrate_withNewRepeatingVibration_cancelsOngoingEffect() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
@@ -880,10 +907,8 @@
         // The second vibration shouldn't have recorded that the vibrators were turned on.
         verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
         // The second vibration shouldn't have played any prebaked segment.
-        assertFalse(
-                mVibratorProviders.get(1).getAllEffectSegments().stream()
-                        .anyMatch(segment -> segment instanceof PrebakedSegment));
-
+        assertFalse(mVibratorProviders.get(1).getAllEffectSegments().stream()
+                .anyMatch(PrebakedSegment.class::isInstance));
         cancelVibrate(service);  // Clean up long effect.
     }
 
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
index c611e38..3e78f9a 100644
--- a/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
+++ b/services/tests/servicestests/test-apps/PackageParserApp/Android.bp
@@ -88,3 +88,17 @@
     resource_dirs: ["res"],
     manifest: "AndroidManifestApp5.xml",
 }
+
+android_test_helper_app {
+    name: "PackageParserTestApp6",
+    sdk_version: "current",
+    srcs: ["**/*.java"],
+    dex_preopt: {
+        enabled: false,
+    },
+    optimize: {
+        enabled: false,
+    },
+    resource_dirs: ["res"],
+    manifest: "AndroidManifestApp6.xml",
+}
diff --git a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml
index 70fd28d..4dcb442 100644
--- a/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml
+++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp4.xml
@@ -31,7 +31,8 @@
         <property android:name="android.cts.PROPERTY_STRING_VIA_RESOURCE" android:value="@string/string_property" />
 
 	    <activity android:name="com.android.servicestests.apps.packageparserapp.MyActivity"
-	              android:exported="true" >
+	              android:exported="true"
+	              android:targetDisplayCategory="automotive">
 	        <property android:name="android.cts.PROPERTY_ACTIVITY" android:value="@integer/integer_property" />
 	        <property android:name="android.cts.PROPERTY_COMPONENT" android:value="@integer/integer_property" />
 	        <property android:name="android.cts.PROPERTY_STRING" android:value="koala activity" />
diff --git a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp6.xml
similarity index 61%
copy from packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
copy to services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp6.xml
index 9e61236..8e694e1 100644
--- a/packages/SystemUI/res-keyguard/drawable/media_squiggly_progress.xml
+++ b/services/tests/servicestests/test-apps/PackageParserApp/AndroidManifestApp6.xml
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <!--
   ~ Copyright (C) 2022 The Android Open Source Project
   ~
@@ -14,4 +15,13 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.systemui.media.SquigglyProgress />
\ No newline at end of file
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.servicestests.apps.packageparserapp" >
+    <application>
+        <activity android:name="com.android.servicestests.apps.packageparserapp.MyActivity"
+                  android:exported="true"
+                  android:targetDisplayCategory="$automotive">
+        </activity>
+    </application>
+</manifest>
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 7986043..582e744 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -62,11 +62,11 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.SparseArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
 import com.google.android.collect.Lists;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index 4b93e35..9c68ddc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -46,10 +46,10 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.IntArray;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 
 import com.android.internal.util.function.TriPredicate;
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.NotificationManagerService.NotificationAssistants;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 248a3fc..581cf43 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -63,10 +63,10 @@
 import android.testing.TestableContext;
 import android.util.ArraySet;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
 import com.google.common.collect.ImmutableList;
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 92761427..2ed8b10 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -193,8 +193,6 @@
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Pair;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.widget.RemoteViews;
 
@@ -206,6 +204,8 @@
 import com.android.internal.logging.InstanceIdSequenceFake;
 import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.statusbar.NotificationVisibility;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
deleted file mode 100644
index d765042..0000000
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java
+++ /dev/null
@@ -1,551 +0,0 @@
-/*
- * Copyright (C) 2017 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.notification;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertNotNull;
-import static junit.framework.Assert.assertNull;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.app.RemoteInput;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.style.StyleSpan;
-import android.util.Pair;
-import android.widget.RemoteViews;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.server.UiServiceTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class NotificationTest extends UiServiceTestCase {
-
-    @Mock
-    ActivityManager mAm;
-
-    @Mock
-    Resources mResources;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    public void testDoesNotStripsExtenders() {
-        Notification.Builder nb = new Notification.Builder(mContext, "channel");
-        nb.extend(new Notification.CarExtender().setColor(Color.RED));
-        nb.extend(new Notification.TvExtender().setChannelId("different channel"));
-        nb.extend(new Notification.WearableExtender().setDismissalId("dismiss"));
-        Notification before = nb.build();
-        Notification after = Notification.Builder.maybeCloneStrippedForDelivery(before);
-
-        assertTrue(before == after);
-
-        assertEquals("different channel", new Notification.TvExtender(before).getChannelId());
-        assertEquals(Color.RED, new Notification.CarExtender(before).getColor());
-        assertEquals("dismiss", new Notification.WearableExtender(before).getDismissalId());
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_noStyles() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test");
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-
-        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_noStyleToStyle() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test");
-        Notification.Builder n2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_styleToNoStyle() {
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testStyleChangeVisiblyDifferent_changeStyle() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle());
-        Notification.Builder n2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle());
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testInboxTextChange() {
-        Notification.Builder nInbox1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle().addLine("a").addLine("b"));
-        Notification.Builder nInbox2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.InboxStyle().addLine("b").addLine("c"));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nInbox1, nInbox2));
-    }
-
-    @Test
-    public void testBigTextTextChange() {
-        Notification.Builder nBigText1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle().bigText("something"));
-        Notification.Builder nBigText2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigTextStyle().bigText("else"));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigText1, nBigText2));
-    }
-
-    @Test
-    public void testBigPictureChange() {
-        Bitmap bitA = mock(Bitmap.class);
-        when(bitA.getGenerationId()).thenReturn(100);
-        Bitmap bitB = mock(Bitmap.class);
-        when(bitB.getGenerationId()).thenReturn(200);
-
-        Notification.Builder nBigPic1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigPictureStyle().bigPicture(bitA));
-        Notification.Builder nBigPic2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.BigPictureStyle().bigPicture(bitB));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nBigPic1, nBigPic2));
-    }
-
-    @Test
-    public void testMessagingChange_text() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class)))
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "b", 100, mock(Person.class)))
-                );
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_data() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))
-                                .setData("text", mock(Uri.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_sender() {
-        Person a = mock(Person.class);
-        when(a.getName()).thenReturn("A");
-        Person b = mock(Person.class);
-        when(b.getName()).thenReturn("b");
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_key() {
-        Person a = mock(Person.class);
-        when(a.getKey()).thenReturn("A");
-        Person b = mock(Person.class);
-        when(b.getKey()).thenReturn("b");
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, a)));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message("a", 100, b)));
-
-        assertTrue(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testMessagingChange_ignoreTimeChange() {
-        Notification.Builder nM1 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 100, mock(Person.class))));
-        Notification.Builder nM2 = new Notification.Builder(mContext, "test")
-                .setStyle(new Notification.MessagingStyle("")
-                        .addMessage(new Notification.MessagingStyle.Message(
-                                "a", 1000, mock(Person.class)))
-                );
-
-        assertFalse(Notification.areStyledNotificationsVisiblyDifferent(nM1, nM2));
-    }
-
-    @Test
-    public void testRemoteViews_nullChange() {
-        Notification.Builder n1 = new Notification.Builder(mContext, "test")
-                .setContent(mock(RemoteViews.class));
-        Notification.Builder n2 = new Notification.Builder(mContext, "test");
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test")
-                .setContent(mock(RemoteViews.class));
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test")
-                .setCustomBigContentView(mock(RemoteViews.class));
-        n2 = new Notification.Builder(mContext, "test");
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test")
-                .setCustomBigContentView(mock(RemoteViews.class));
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test");
-        n2 = new Notification.Builder(mContext, "test");
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_layoutChange() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(189);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_layoutSame() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_sequenceChange() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        when(a.getSequenceNumber()).thenReturn(1);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-        when(b.getSequenceNumber()).thenReturn(2);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertTrue(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testRemoteViews_sequenceSame() {
-        RemoteViews a = mock(RemoteViews.class);
-        when(a.getLayoutId()).thenReturn(234);
-        when(a.getSequenceNumber()).thenReturn(1);
-        RemoteViews b = mock(RemoteViews.class);
-        when(b.getLayoutId()).thenReturn(234);
-        when(b.getSequenceNumber()).thenReturn(1);
-
-        Notification.Builder n1 = new Notification.Builder(mContext, "test").setContent(a);
-        Notification.Builder n2 = new Notification.Builder(mContext, "test").setContent(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomBigContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomBigContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-
-        n1 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(a);
-        n2 = new Notification.Builder(mContext, "test").setCustomHeadsUpContentView(b);
-        assertFalse(Notification.areRemoteViewsChanged(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferent_null() {
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentSame() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentText() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
-                .build();
-
-        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentSpannables() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon,
-                        new SpannableStringBuilder().append("test1",
-                                new StyleSpan(Typeface.BOLD),
-                                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE),
-                        intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "test1", intent).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentNumber() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent).build())
-                .addAction(new Notification.Action.Builder(icon, "TEXT 2", intent).build())
-                .build();
-
-        assertTrue(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsDifferentIntent() {
-        PendingIntent intent1 = mock(PendingIntent.class);
-        PendingIntent intent2 = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent1).build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent2).build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testActionsIgnoresRemoteInputs() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        Notification n1 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(new RemoteInput.Builder("a")
-                                .setChoices(new CharSequence[] {"i", "m"})
-                                .build())
-                        .build())
-                .build();
-        Notification n2 = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(new RemoteInput.Builder("a")
-                                .setChoices(new CharSequence[] {"t", "m"})
-                                .build())
-                        .build())
-                .build();
-
-        assertFalse(Notification.areActionsVisiblyDifferent(n1, n2));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_noRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .build())
-                .build();
-        assertNull(notification.findRemoteInputActionPair(false));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_hasRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        RemoteInput remoteInput = new RemoteInput.Builder("a").build();
-
-        Notification.Action actionWithRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(remoteInput)
-                        .addRemoteInput(remoteInput)
-                        .build();
-
-        Notification.Action actionWithoutRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 2", intent)
-                        .build();
-
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(actionWithoutRemoteInput)
-                .addAction(actionWithRemoteInput)
-                .build();
-
-        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
-                notification.findRemoteInputActionPair(false);
-
-        assertNotNull(remoteInputActionPair);
-        assertEquals(remoteInput, remoteInputActionPair.first);
-        assertEquals(actionWithRemoteInput, remoteInputActionPair.second);
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_requestFreeform_noFreeformRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(
-                                new RemoteInput.Builder("a")
-                                        .setAllowFreeFormInput(false).build())
-                        .build())
-                .build();
-        assertNull(notification.findRemoteInputActionPair(true));
-    }
-
-    @Test
-    public void testFreeformRemoteInputActionPair_requestFreeform_hasFreeformRemoteInput() {
-        PendingIntent intent = mock(PendingIntent.class);
-        Icon icon = mock(Icon.class);
-
-        RemoteInput remoteInput =
-                new RemoteInput.Builder("a").setAllowFreeFormInput(false).build();
-        RemoteInput freeformRemoteInput =
-                new RemoteInput.Builder("b").setAllowFreeFormInput(true).build();
-
-        Notification.Action actionWithFreeformRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 1", intent)
-                        .addRemoteInput(remoteInput)
-                        .addRemoteInput(freeformRemoteInput)
-                        .build();
-
-        Notification.Action actionWithoutFreeformRemoteInput =
-                new Notification.Action.Builder(icon, "TEXT 2", intent)
-                        .addRemoteInput(remoteInput)
-                        .build();
-
-        Notification notification = new Notification.Builder(mContext, "test")
-                .addAction(actionWithoutFreeformRemoteInput)
-                .addAction(actionWithFreeformRemoteInput)
-                .build();
-
-        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
-                notification.findRemoteInputActionPair(true);
-
-        assertNotNull(remoteInputActionPair);
-        assertEquals(freeformRemoteInput, remoteInputActionPair.first);
-        assertEquals(actionWithFreeformRemoteInput, remoteInputActionPair.second);
-    }
-}
-
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 598a22b..0f93598 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -110,14 +110,14 @@
 import android.util.IntArray;
 import android.util.Pair;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.os.AtomsProto.PackageNotificationPreferences;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.PermissionHelper.PackagePermission;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 7817e81..a03a1b4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -44,12 +44,12 @@
 import android.service.notification.StatusBarNotification;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.IntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 import com.android.server.pm.PackageManagerService;
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 949455a1..2b6db14 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -33,12 +33,12 @@
 import android.service.notification.ZenModeConfig.EventInfo;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 
 import org.junit.Test;
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 2ccdcaa..49edde5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -105,12 +105,12 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.StatsEvent;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.R;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.ManagedServices.UserProfiles;
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c3d49e1..bc319db 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -242,7 +242,7 @@
     private IOnBackInvokedCallback createOnBackInvokedCallback() {
         return new IOnBackInvokedCallback.Stub() {
             @Override
-            public void onBackStarted() {
+            public void onBackStarted(BackEvent backEvent) {
             }
 
             @Override
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 11ae5d4..e69418b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2314,6 +2314,8 @@
         assertEquals(displayWidth, windowConfig.getBounds().width());
         assertEquals(displayHeight, windowConfig.getBounds().height());
         assertEquals(windowingMode, windowConfig.getWindowingMode());
+        assertEquals(Configuration.SCREENLAYOUT_SIZE_NORMAL,
+                config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
 
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
@@ -2355,6 +2357,8 @@
         assertEquals(displayWidth, windowConfig.getBounds().width());
         assertEquals(displayHeight, windowConfig.getBounds().height());
         assertEquals(windowingMode, windowConfig.getWindowingMode());
+        assertEquals(Configuration.SCREENLAYOUT_SIZE_LARGE, testDisplayContent
+                .getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK);
 
         // test misc display overrides
         assertEquals(ignoreOrientationRequests, testDisplayContent.mSetIgnoreOrientationRequest);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 18a1caa..9d839fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -29,7 +29,6 @@
 
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
-import android.util.TypedXmlPullParser;
 import android.util.Xml;
 import android.view.Display;
 import android.view.DisplayAddress;
@@ -37,6 +36,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.modules.utils.TypedXmlPullParser;
 import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
 
 import org.junit.After;
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 0b23359..4202f46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -56,6 +56,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -91,6 +92,7 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 
 import java.util.List;
 
@@ -762,6 +764,50 @@
     }
 
     @Test
+    public void testOrganizerRemovedWithPendingEvents() {
+        final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        final TaskFragment tf1 = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        assertTrue(tf0.isOrganizedTaskFragment());
+        assertTrue(tf1.isOrganizedTaskFragment());
+        assertTrue(tf0.isAttached());
+        assertTrue(tf0.isAttached());
+
+        // Mock the behavior that remove TaskFragment can trigger event dispatch.
+        final Answer<Void> removeImmediately = invocation -> {
+            invocation.callRealMethod();
+            mController.dispatchPendingEvents();
+            return null;
+        };
+        doAnswer(removeImmediately).when(tf0).removeImmediately();
+        doAnswer(removeImmediately).when(tf1).removeImmediately();
+
+        // Add pending events.
+        mController.onTaskFragmentAppeared(mIOrganizer, tf0);
+        mController.onTaskFragmentAppeared(mIOrganizer, tf1);
+
+        // Remove organizer.
+        mController.unregisterOrganizer(mIOrganizer);
+        mController.dispatchPendingEvents();
+
+        // Nothing should happen after the organizer is removed.
+        verify(mOrganizer, never()).onTransactionReady(any());
+
+        // TaskFragments should be removed.
+        assertFalse(tf0.isOrganizedTaskFragment());
+        assertFalse(tf1.isOrganizedTaskFragment());
+        assertFalse(tf0.isAttached());
+        assertFalse(tf0.isAttached());
+    }
+
+    @Test
     public void testTaskFragmentInPip_startActivityInTaskFragment() {
         setupTaskFragmentInPip();
         final ActivityRecord activity = mTaskFragment.getTopMostActivity();
@@ -874,29 +920,87 @@
 
     @Test
     public void testDeferPendingTaskFragmentEventsOfInvisibleTask() {
-        // Task - TaskFragment - Activity.
         final Task task = createTask(mDisplayContent);
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setParentTask(task)
                 .setOrganizer(mOrganizer)
                 .setFragmentToken(mFragmentToken)
                 .build();
-
-        // Mock the task to invisible
         doReturn(false).when(task).shouldBeVisible(any());
 
-        // Sending events
-        taskFragment.mTaskFragmentAppearedSent = true;
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+
+        // Verify that events were not sent when the Task is in background.
+        clearInvocations(mOrganizer);
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
+        mController.onTaskFragmentParentInfoChanged(mIOrganizer, task);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-
-        // Verifies that event was not sent
         verify(mOrganizer, never()).onTransactionReady(any());
+
+        // Verify that the events were sent when the Task becomes visible.
+        doReturn(true).when(task).shouldBeVisible(any());
+        task.lastActiveTime++;
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+    }
+
+    @Test
+    public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() {
+        // Invisible Task.
+        final Task invisibleTask = createTask(mDisplayContent);
+        final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(invisibleTask)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        doReturn(false).when(invisibleTask).shouldBeVisible(any());
+
+        // Visible Task.
+        final IBinder fragmentToken = new Binder();
+        final Task visibleTask = createTask(mDisplayContent);
+        final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(visibleTask)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(fragmentToken)
+                .build();
+        doReturn(true).when(invisibleTask).shouldBeVisible(any());
+
+        // Sending events
+        invisibleTaskFragment.mTaskFragmentAppearedSent = true;
+        visibleTaskFragment.mTaskFragmentAppearedSent = true;
+        mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment);
+        mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment);
+        mController.dispatchPendingEvents();
+
+        // Verify that both events are sent.
+        verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
+        final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
+        final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
+
+        // There should be two Task info changed with two TaskFragment info changed.
+        assertEquals(4, changes.size());
+        // Invisible Task info changed
+        assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType());
+        assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId());
+        // Invisible TaskFragment info changed
+        assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType());
+        assertEquals(invisibleTaskFragment.getFragmentToken(),
+                changes.get(1).getTaskFragmentToken());
+        // Visible Task info changed
+        assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType());
+        assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId());
+        // Visible TaskFragment info changed
+        assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType());
+        assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken());
     }
 
     @Test
     public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() {
-        // Task - TaskFragment - Activity.
         final Task task = createTask(mDisplayContent);
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setParentTask(task)
@@ -905,24 +1009,26 @@
                 .createActivityCount(1)
                 .build();
         final ActivityRecord activity = taskFragment.getTopMostActivity();
-
-        // Mock the task to invisible
         doReturn(false).when(task).shouldBeVisible(any());
         taskFragment.setResumedActivity(null, "test");
 
-        // Sending events
-        taskFragment.mTaskFragmentAppearedSent = true;
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+
+        // Verify the info changed event is not sent because the Task is invisible
+        clearInvocations(mOrganizer);
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-
-        // Verifies that event was not sent
         verify(mOrganizer, never()).onTransactionReady(any());
 
-        // Mock the task becomes visible, and activity resumed
+        // Mock the task becomes visible, and activity resumed. Verify the info changed event is
+        // sent.
         doReturn(true).when(task).shouldBeVisible(any());
         taskFragment.setResumedActivity(activity, "test");
-
-        // Verifies that event is sent.
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
     }
@@ -977,25 +1083,24 @@
         final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
         // Add another activity in the Task so that it always contains a non-finishing activity.
         createActivityRecord(task);
-        assertTrue(task.shouldBeVisible(null));
+        doReturn(false).when(task).shouldBeVisible(any());
 
-        // Dispatch pending info changed event from creating the activity
-        taskFragment.mTaskFragmentAppearedSent = true;
-        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
 
-        // Verify the info changed callback is not called when the task is invisible
+        // Verify the info changed event is not sent because the Task is invisible
         clearInvocations(mOrganizer);
-        doReturn(false).when(task).shouldBeVisible(any());
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer, never()).onTransactionReady(any());
 
-        // Finish the embedded activity, and verify the info changed callback is called because the
+        // Finish the embedded activity, and verify the info changed event is sent because the
         // TaskFragment is becoming empty.
         embeddedActivity.finishing = true;
-        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 92c9e80..66bf78b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -77,14 +77,15 @@
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.util.DisplayMetrics;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 import android.view.Display;
 import android.view.DisplayInfo;
 
 import androidx.test.filters.MediumTest;
 
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
index fc3962b..cd4d65d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java
@@ -26,8 +26,10 @@
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.hardware.HardwareBuffer;
-import android.view.Surface;
 import android.platform.test.annotations.Presubmit;
+import android.view.Surface;
+
+import com.android.internal.policy.TransitionAnimation;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,7 +54,8 @@
     public void blackLuma() {
         Bitmap swBitmap = createBitmap(0);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
+
         assertEquals(0, borderLuma, 0);
     }
 
@@ -60,7 +63,7 @@
     public void whiteLuma() {
         Bitmap swBitmap = createBitmap(1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
@@ -68,7 +71,7 @@
     public void unevenBitmapDimens() {
         Bitmap swBitmap = createBitmap(1, BITMAP_WIDTH + 1, BITMAP_HEIGHT + 1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
@@ -77,7 +80,7 @@
         Bitmap swBitmap = createBitmap(1);
         setBorderLuma(swBitmap, 0);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(0, borderLuma, 0);
     }
 
@@ -86,7 +89,7 @@
         Bitmap swBitmap = createBitmap(0);
         setBorderLuma(swBitmap, 1);
         HardwareBuffer hb = swBitmapToHardwareBuffer(swBitmap);
-        float borderLuma = RotationAnimationUtils.getMedianBorderLuma(hb, mColorSpace);
+        float borderLuma = TransitionAnimation.getBorderLuma(hb, mColorSpace);
         assertEquals(1, borderLuma, 0);
     }
 
diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
index 2ae328b..394d6e7 100644
--- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.usb.UsbConfiguration;
+import android.hardware.usb.UsbConstants;
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbDeviceConnection;
 import android.hardware.usb.UsbEndpoint;
@@ -76,10 +77,10 @@
     // event schedulers for each input port of the physical device
     private MidiEventScheduler[] mEventSchedulers;
 
-    // Arbitrary number for timeout to not continue sending to
-    // an inactive device. This number tries to balances the number
-    // of cycles and not being permanently stuck.
-    private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 10;
+    // Timeout for sending a packet to a device.
+    // If bulkTransfer times out, retry sending the packet up to 20 times.
+    private static final int BULK_TRANSFER_TIMEOUT_MILLISECONDS = 50;
+    private static final int BULK_TRANSFER_NUMBER_OF_RETRIES = 20;
 
     // Arbitrary number for timeout when closing a thread
     private static final int THREAD_JOIN_TIMEOUT_MILLISECONDS = 200;
@@ -386,10 +387,15 @@
                                     break;
                                 }
                                 final UsbRequest response = connectionFinal.requestWait();
-                                if (response != request) {
-                                    Log.w(TAG, "Unexpected response");
+                                if (response == null) {
+                                    Log.w(TAG, "Response is null");
                                     break;
                                 }
+                                if (request != response) {
+                                    Log.w(TAG, "Skipping response");
+                                    continue;
+                                }
+
                                 int bytesRead = byteBuffer.position();
 
                                 if (bytesRead > 0) {
@@ -513,9 +519,47 @@
                                             convertedArray.length);
                                 }
 
-                                connectionFinal.bulkTransfer(endpointFinal, convertedArray,
-                                        convertedArray.length,
-                                        BULK_TRANSFER_TIMEOUT_MILLISECONDS);
+                                boolean isInterrupted = false;
+                                // Split the packet into multiple if they are greater than the
+                                // endpoint's max packet size.
+                                for (int curPacketStart = 0;
+                                        curPacketStart < convertedArray.length &&
+                                        isInterrupted == false;
+                                        curPacketStart += endpointFinal.getMaxPacketSize()) {
+                                    int transferResult = -1;
+                                    int retryCount = 0;
+                                    int curPacketSize = Math.min(endpointFinal.getMaxPacketSize(),
+                                            convertedArray.length - curPacketStart);
+
+                                    // Keep trying to send the packet until the result is
+                                    // successful or until the retry limit is reached.
+                                    while (transferResult < 0 && retryCount <=
+                                            BULK_TRANSFER_NUMBER_OF_RETRIES) {
+                                        transferResult = connectionFinal.bulkTransfer(
+                                                endpointFinal,
+                                                convertedArray,
+                                                curPacketStart,
+                                                curPacketSize,
+                                                BULK_TRANSFER_TIMEOUT_MILLISECONDS);
+                                        retryCount++;
+
+                                        if (Thread.currentThread().interrupted()) {
+                                            Log.w(TAG, "output thread interrupted after send");
+                                            isInterrupted = true;
+                                            break;
+                                        }
+                                        if (transferResult < 0) {
+                                            Log.d(TAG, "retrying packet. retryCount = "
+                                                    + retryCount + " result = " + transferResult);
+                                            if (retryCount > BULK_TRANSFER_NUMBER_OF_RETRIES) {
+                                                Log.w(TAG, "Skipping packet because timeout");
+                                            }
+                                        }
+                                    }
+                                }
+                                if (isInterrupted == true) {
+                                    break;
+                                }
                                 eventSchedulerFinal.addEventToPool(event);
                             }
                         } catch (NullPointerException e) {
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index bb0c4e9..47b09fe 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -53,8 +53,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
@@ -62,6 +60,8 @@
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 
 import libcore.io.IoUtils;
 
diff --git a/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java b/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java
index dd5f153..f39cb39 100644
--- a/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java
+++ b/services/usb/java/com/android/server/usb/UsbUserPermissionManager.java
@@ -48,13 +48,13 @@
 import android.util.EventLog;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.dump.DualDumpOutputStream;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
 
 import org.xmlpull.v1.XmlPullParser;
diff --git a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
index 5179bab..76d2b7d 100644
--- a/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
+++ b/telephony/common/com/android/internal/telephony/util/TelephonyUtils.java
@@ -206,6 +206,8 @@
                 return "DATA_ON_NON_DEFAULT_DURING_VOICE_CALL";
             case TelephonyManager.MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED:
                 return "MMS_ALWAYS_ALLOWED";
+            case TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH:
+                return "AUTO_DATA_SWITCH";
             default:
                 return "UNKNOWN(" + mobileDataPolicy + ")";
         }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f43f0a5..d314a65 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8658,11 +8658,12 @@
 
     /**
      * Boolean indicating if the VoNR setting is visible in the Call Settings menu.
-     * If true, the VoNR setting menu will be visible. If false, the menu will be gone.
+     * If this flag is set and VoNR is enabled for this carrier (see {@link #KEY_VONR_ENABLED_BOOL})
+     * the VoNR setting menu will be visible. If {@link #KEY_VONR_ENABLED_BOOL} or
+     * this setting is false, the menu will be gone.
      *
-     * Disabled by default.
+     * Enabled by default.
      *
-     * @hide
      */
     public static final String KEY_VONR_SETTING_VISIBILITY_BOOL = "vonr_setting_visibility_bool";
 
@@ -8672,7 +8673,6 @@
      *
      * Disabled by default.
      *
-     * @hide
      */
     public static final String KEY_VONR_ENABLED_BOOL = "vonr_enabled_bool";
 
@@ -8715,6 +8715,8 @@
      * premium capabilities should be blocked when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to user action or timeout.
+     * The maximum number of network boost notifications to show the user are defined in
+     * {@link #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}.
      *
      * The default value is 30 minutes.
      *
@@ -8726,6 +8728,22 @@
             "premium_capability_notification_backoff_hysteresis_time_millis_long";
 
     /**
+     * The maximum number of times that we display the notification for a network boost via premium
+     * capabilities when {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * returns a failure due to user action or timeout.
+     *
+     * An int array with 2 values: {max_notifications_per_day, max_notifications_per_month}.
+     *
+     * The default value is {2, 10}, meaning we display a maximum of 2 network boost notifications
+     * per day and 10 notifications per month.
+     *
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED
+     * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY =
+            "premium_capability_maximum_notification_count_int_array";
+
+    /**
      * The amount of time in milliseconds that the purchase request should be throttled when
      * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
      * returns a failure due to the carrier.
@@ -8752,6 +8770,20 @@
             "premium_capability_purchase_url_string";
 
     /**
+     * Whether to allow premium capabilities to be purchased when the device is connected to LTE.
+     * If this is {@code true}, applications can call
+     * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+     * when connected to {@link TelephonyManager#NETWORK_TYPE_LTE} to purchase and use
+     * premium capabilities.
+     * If this is {@code false}, applications can only purchase and use premium capabilities when
+     * conencted to {@link TelephonyManager#NETWORK_TYPE_NR}.
+     *
+     * This is {@code false} by default.
+     */
+    public static final String KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL =
+            "premium_capability_supported_on_lte_bool";
+
+    /**
      * IWLAN handover rules that determine whether handover is allowed or disallowed between
      * cellular and IWLAN.
      *
@@ -9432,15 +9464,18 @@
         sDefaults.putBoolean(KEY_UNTHROTTLE_DATA_RETRY_WHEN_TAC_CHANGES_BOOL, false);
         sDefaults.putBoolean(KEY_VONR_SETTING_VISIBILITY_BOOL, true);
         sDefaults.putBoolean(KEY_VONR_ENABLED_BOOL, false);
-        sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[]{});
+        sDefaults.putIntArray(KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY, new int[] {});
         sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
         sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
+        sDefaults.putIntArray(KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY,
+                new int[] {2, 10});
         sDefaults.putLong(
                 KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
                 TimeUnit.MINUTES.toMillis(30));
         sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null);
+        sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false);
         sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{
                 "source=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, "
                         + "target=GERAN|UTRAN|EUTRAN|NGRAN|IWLAN, type=allowed"});
diff --git a/telephony/java/android/telephony/PhoneCapability.java b/telephony/java/android/telephony/PhoneCapability.java
index 63e3468..48170df 100644
--- a/telephony/java/android/telephony/PhoneCapability.java
+++ b/telephony/java/android/telephony/PhoneCapability.java
@@ -90,7 +90,9 @@
     /**
      * mMaxActiveVoiceSubscriptions defines the maximum subscriptions that can support
      * simultaneous voice calls. For a dual sim dual standby (DSDS) device it would be one, but
-     * for a dual sim dual active device it would be 2.
+     * for a dual sim dual active (DSDA) device, or a DSDS device that supports "virtual DSDA" (
+     * using the data line of 1 SIM to temporarily provide IMS voice connectivity to the other SIM)
+     * it would be 2.
      *
      * @hide
      */
@@ -99,7 +101,7 @@
     /**
      * mMaxActiveDataSubscriptions defines the maximum subscriptions that can support
      * simultaneous data connections.
-     * For example, for L+L device it should be 2.
+     * For example, for dual sim dual active L+L device it should be 2.
      *
      * @hide
      */
@@ -114,14 +116,20 @@
      */
     private final boolean mNetworkValidationBeforeSwitchSupported;
 
-    /** @hide */
-    private final List<ModemInfo> mLogicalModemList;
-
     /**
      * List of logical modem information.
      *
      * @hide
      */
+    @NonNull
+    private final List<ModemInfo> mLogicalModemList;
+
+    /**
+     * Device NR capabilities.
+     *
+     * @hide
+     */
+    @NonNull
     private final int[] mDeviceNrCapabilities;
 
     /** @hide */
@@ -136,6 +144,18 @@
         this.mDeviceNrCapabilities = deviceNrCapabilities;
     }
 
+    private PhoneCapability(@NonNull Builder builder) {
+        this.mMaxActiveVoiceSubscriptions = builder.mMaxActiveVoiceSubscriptions;
+        this.mMaxActiveDataSubscriptions = builder.mMaxActiveDataSubscriptions;
+        // Make sure it's not null.
+        this.mLogicalModemList = builder.mLogicalModemList == null ? new ArrayList<>()
+                : builder.mLogicalModemList;
+        this.mNetworkValidationBeforeSwitchSupported =
+                builder.mNetworkValidationBeforeSwitchSupported;
+        this.mDeviceNrCapabilities = builder.mDeviceNrCapabilities;
+
+    }
+
     @Override
     public String toString() {
         return "mMaxActiveVoiceSubscriptions=" + mMaxActiveVoiceSubscriptions
@@ -264,4 +284,121 @@
     public @NonNull @DeviceNrCapability int[] getDeviceNrCapabilities() {
         return mDeviceNrCapabilities == null ? (new int[0]) : mDeviceNrCapabilities;
     }
+
+
+    /**
+     * Builder for {@link PhoneCapability}.
+     *
+     * @hide
+     */
+    public static class Builder {
+        /**
+         * mMaxActiveVoiceSubscriptions defines the maximum subscriptions that can support
+         * simultaneous voice calls. For a dual sim dual standby (DSDS) device it would be one, but
+         * for a dual sim dual active (DSDA) device, or a DSDS device that supports "virtual DSDA"
+         * (using the data line of 1 SIM to temporarily provide IMS voice connectivity to the other
+         * SIM) it would be 2.
+         *
+         * @hide
+         */
+        private int mMaxActiveVoiceSubscriptions = 0;
+
+        /**
+         * mMaxActiveDataSubscriptions defines the maximum subscriptions that can support
+         * simultaneous data connections. For example, for L+L device it should be 2.
+         *
+         * @hide
+         */
+        private int mMaxActiveDataSubscriptions = 0;
+
+        /**
+         * Whether modem supports both internet PDN up so that we can do ping test before tearing
+         * down the other one.
+         *
+         * @hide
+         */
+        private boolean mNetworkValidationBeforeSwitchSupported = false;
+
+        /**
+         * List of logical modem information.
+         *
+         * @hide
+         */
+        @NonNull
+        private List<ModemInfo> mLogicalModemList = new ArrayList<>();
+
+        /**
+         * Device NR capabilities.
+         *
+         * @hide
+         */
+        @NonNull
+        private int[] mDeviceNrCapabilities = new int[0];
+
+        /**
+         * Default constructor.
+         */
+        public Builder() {
+        }
+
+        public Builder(@NonNull PhoneCapability phoneCapability) {
+            mMaxActiveVoiceSubscriptions = phoneCapability.mMaxActiveVoiceSubscriptions;
+            mMaxActiveDataSubscriptions = phoneCapability.mMaxActiveDataSubscriptions;
+            mNetworkValidationBeforeSwitchSupported =
+                    phoneCapability.mNetworkValidationBeforeSwitchSupported;
+            mLogicalModemList = phoneCapability.mLogicalModemList;
+            mDeviceNrCapabilities = phoneCapability.mDeviceNrCapabilities;
+        }
+
+        /**
+         * Sets the max active voice subscriptions supported by the device.
+         */
+        public Builder setMaxActiveVoiceSubscriptions(int maxActiveVoiceSubscriptions) {
+            mMaxActiveVoiceSubscriptions = maxActiveVoiceSubscriptions;
+            return this;
+        }
+
+        /**
+         * Sets the max active voice subscriptions supported by the device.
+         */
+        public Builder setMaxActiveDataSubscriptions(int maxActiveDataSubscriptions) {
+            mMaxActiveDataSubscriptions = maxActiveDataSubscriptions;
+            return this;
+        }
+
+        /**
+         * Sets the max active data subscriptions supported by the device. Can be fewer than the
+         * active voice subscriptions.
+         */
+        public Builder setNetworkValidationBeforeSwitchSupported(
+                boolean networkValidationBeforeSwitchSupported) {
+            mNetworkValidationBeforeSwitchSupported = networkValidationBeforeSwitchSupported;
+            return this;
+        }
+
+        /**
+         * Sets the logical modem list of the device.
+         */
+        public Builder setLogicalModemList(@NonNull List<ModemInfo> logicalModemList) {
+            mLogicalModemList = logicalModemList;
+            return this;
+        }
+
+        /**
+         * Sets the NR capabilities supported by the device.
+         */
+        public Builder setDeviceNrCapabilities(@NonNull int[] deviceNrCapabilities) {
+            mDeviceNrCapabilities = deviceNrCapabilities;
+            return this;
+        }
+
+        /**
+         * Build the {@link PhoneCapability}.
+         *
+         * @return The {@link PhoneCapability} instance.
+         */
+        public PhoneCapability build() {
+            return new PhoneCapability(this);
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index eb3affc..439eaa6 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -54,6 +54,7 @@
 import android.os.ParcelUuid;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.provider.Telephony.SimInfo;
 import android.telephony.euicc.EuiccManager;
 import android.telephony.ims.ImsMmTelManager;
@@ -4154,4 +4155,79 @@
                 return "UNKNOWN(" + usageSetting + ")";
         }
     }
+
+    /**
+     * Set userHandle for a subscription.
+     *
+     * Used to set an association between a subscription and a user on the device so that voice
+     * calling and SMS from that subscription can be associated with that user.
+     * Data services are always shared between users on the device.
+     *
+     * @param subscriptionId the subId of the subscription.
+     * @param userHandle the userHandle associated with the subscription.
+     * Pass {@code null} user handle to clear the association.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: "
+                    + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName());
+            } else {
+                throw new IllegalStateException("[setUserHandle]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Get UserHandle of this subscription.
+     *
+     * Used to get user handle associated with this subscription.
+     *
+     * @param subscriptionId the subId of the subscription.
+     * @return userHandle associated with this subscription
+     * or {@code null} if subscription is not associated with any user.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public @Nullable UserHandle getUserHandle(int subscriptionId) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: "
+                    + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName());
+            } else {
+                throw new IllegalStateException("[getUserHandle]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+        return null;
+    }
 }
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index f3d48a8..9ecebf1 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -54,6 +54,7 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -15626,11 +15627,29 @@
     public static final int MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED = 2;
 
     /**
+     * Allow switching mobile data to the non-default SIM if the non-default SIM has better
+     * availability.
+     *
+     * This is used for temporarily allowing data on the non-default data SIM when on-default SIM
+     * has better availability on DSDS devices, where better availability means strong
+     * signal/connectivity.
+     * If this policy is enabled, data will be temporarily enabled on the non-default data SIM,
+     * including during any voice calls(equivalent to enabling
+     * {@link #MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL}).
+     *
+     * This policy can be enabled and disabled via {@link #setMobileDataPolicyEnabled}.
+     * @hide
+     */
+    @SystemApi
+    public static final int MOBILE_DATA_POLICY_AUTO_DATA_SWITCH = 3;
+
+    /**
      * @hide
      */
     @IntDef(prefix = { "MOBILE_DATA_POLICY_" }, value = {
             MOBILE_DATA_POLICY_DATA_ON_NON_DEFAULT_DURING_VOICE_CALL,
             MOBILE_DATA_POLICY_MMS_ALWAYS_ALLOWED,
+            MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface MobileDataPolicy { }
@@ -17115,11 +17134,12 @@
     }
 
     /**
-     * A premium capability boosting the network to allow real-time interactive traffic.
-     * Corresponds to NetworkCapabilities#NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC.
+     * A premium capability that boosts the network to allow for real-time interactive traffic
+     * by prioritizing low latency communication.
+     * Corresponds to {@link NetworkCapabilities#NET_CAPABILITY_PRIORITIZE_LATENCY}.
      */
-    // TODO(b/245748544): add @link once NET_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC is defined.
-    public static final int PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC = 1;
+    public static final int PREMIUM_CAPABILITY_PRIORITIZE_LATENCY =
+            NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY;
 
     /**
      * Purchasable premium capabilities.
@@ -17127,7 +17147,7 @@
      */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = { "PREMIUM_CAPABILITY_" }, value = {
-            PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC})
+            PREMIUM_CAPABILITY_PRIORITIZE_LATENCY})
     public @interface PremiumCapability {}
 
     /**
@@ -17139,8 +17159,8 @@
      */
     public static String convertPremiumCapabilityToString(@PremiumCapability int capability) {
         switch (capability) {
-            case PREMIUM_CAPABILITY_REALTIME_INTERACTIVE_TRAFFIC:
-                return "REALTIME_INTERACTIVE_TRAFFIC";
+            case PREMIUM_CAPABILITY_PRIORITIZE_LATENCY:
+                return "PRIORITIZE_LATENCY";
             default:
                 return "UNKNOWN (" + capability + ")";
         }
@@ -17178,11 +17198,18 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1;
 
     /**
-     * Purchase premium capability failed because the request is throttled for the amount of time
+     * Purchase premium capability failed because the request is throttled.
+     * If purchasing premium capabilities is throttled, it will be for the amount of time
      * specified by {@link CarrierConfigManager
-     * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
-     * or {@link CarrierConfigManager
      * #KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}.
+     * If displaying the network boost notification is throttled, it will be for the amount of time
+     * specified by {@link CarrierConfigManager
+     * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_INT_ARRAY}.
+     * If a foreground application requests premium capabilities, the network boost notification
+     * will be displayed to the user regardless of the throttled status.
+     * We will show the network boost notification to the user up to the daily and monthly maximum
+     * number of times specified by {@link CarrierConfigManager
+     * #KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY}.
      * Subsequent attempts will return the same error until the request is no longer throttled
      * or throttling conditions change.
      */
@@ -17202,10 +17229,14 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS = 4;
 
     /**
-     * Purchase premium capability failed because the user disabled the feature.
-     * Subsequent attempts will return the same error until the user re-enables the feature.
+     * Purchase premium capability failed because a foreground application requested the same
+     * capability. The notification for the current application will be dismissed and a new
+     * notification will be displayed to the user for the foreground application.
+     * Subsequent attempts will return
+     * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS} until the foreground
+     * application's request is completed.
      */
-    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 5;
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5;
 
     /**
      * Purchase premium capability failed because the user canceled the operation.
@@ -17252,7 +17283,8 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED = 10;
 
     /**
-     * Purchase premium capability failed because the telephony service is down or unavailable.
+     * Purchase premium capability failed because the telephony service is unavailable
+     * or there was an error in the phone process.
      * Subsequent attempts will return the same error until request conditions are satisfied.
      */
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11;
@@ -17274,6 +17306,14 @@
     public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED = 13;
 
     /**
+     * Purchase premium capability failed because the request was not made on the default data
+     * subscription, indicated by {@link SubscriptionManager#getDefaultDataSubscriptionId()}.
+     * Subsequent attempts will return the same error until the request is made on the default
+     * data subscription.
+     */
+    public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14;
+
+    /**
      * Results of the purchase premium capability request.
      * @hide
      */
@@ -17283,14 +17323,15 @@
             PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_ERROR,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
             PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
-            PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED})
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED,
+            PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA})
     public @interface PurchasePremiumCapabilityResult {}
 
     /**
@@ -17311,8 +17352,8 @@
                 return "ALREADY_PURCHASED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_IN_PROGRESS:
                 return "ALREADY_IN_PROGRESS";
-            case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED:
-                return "USER_DISABLED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN:
+                return "OVERRIDDEN";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED:
                 return "USER_CANCELED";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_CARRIER_DISABLED:
@@ -17329,6 +17370,8 @@
                 return "NETWORK_NOT_AVAILABLE";
             case PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED:
                 return "NETWORK_CONGESTED";
+            case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA:
+                return "NOT_DEFAULT_DATA";
             default:
                 return "UNKNOWN (" + result + ")";
         }
@@ -17346,7 +17389,7 @@
      * @param callback The result of the purchase request.
      *                 One of {@link PurchasePremiumCapabilityResult}.
      * @throws SecurityException if the caller does not hold permission READ_BASIC_PHONE_STATE.
-     * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid
+     * @see #isPremiumCapabilityAvailableForPurchase(int) to check whether the capability is valid.
      */
     @RequiresPermission(android.Manifest.permission.READ_BASIC_PHONE_STATE)
     public void purchasePremiumCapability(@PremiumCapability int capability,
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 917f35b..0211a7f 100755
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -18,6 +18,7 @@
 
 import android.telephony.SubscriptionInfo;
 import android.os.ParcelUuid;
+import android.os.UserHandle;
 import com.android.internal.telephony.ISetOpportunisticDataCallback;
 
 interface ISub {
@@ -316,4 +317,28 @@
      * @throws SecurityException if doesn't have MODIFY_PHONE_STATE or Carrier Privileges
      */
     int setUsageSetting(int usageSetting, int subId, String callingPackage);
+
+     /**
+      * Set userHandle for this subscription.
+      *
+      * @param userHandle the user handle for this subscription
+      * @param subId the unique SubscriptionInfo index in database
+      * @param callingPackage The package making the IPC.
+      *
+      * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION
+      * @throws IllegalArgumentException if subId is invalid.
+      */
+    int setUserHandle(in UserHandle userHandle, int subId, String callingPackage);
+
+    /**
+     * Get UserHandle for this subscription
+     *
+     * @param subId the unique SubscriptionInfo index in database
+     * @param callingPackage the package making the IPC
+     * @return userHandle associated with this subscription.
+     *
+     * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION
+     * @throws IllegalArgumentException if subId is invalid.
+     */
+     UserHandle getUserHandle(int subId, String callingPackage);
 }
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index 96bbf82..f8d885a 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -44,14 +44,14 @@
 import android.provider.DeviceConfig;
 import android.util.AtomicFile;
 import android.util.LongArrayQueue;
-import android.util.TypedXmlPullParser;
-import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
 import androidx.test.InstrumentationRegistry;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.PackageWatchdog.HealthCheckState;
 import com.android.server.PackageWatchdog.MonitoredPackage;
 import com.android.server.PackageWatchdog.PackageHealthObserver;
diff --git a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
index 916551a..79a2f1f 100644
--- a/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
+++ b/tests/RollbackTest/SampleRollbackApp/src/com/android/sample/rollbackapp/MainActivity.java
@@ -75,6 +75,7 @@
                         String rollbackStatus = "FAILED";
                         if (rollbackStatusCode == RollbackManager.STATUS_SUCCESS) {
                             rollbackStatus = "SUCCESS";
+                            mTriggerRollbackButton.setClickable(false);
                         }
                         makeToast("Status for rollback ID " + rollbackId + " is " + rollbackStatus);
                     }}, new IntentFilter(ACTION_NAME), Context.RECEIVER_NOT_EXPORTED);